Tornado Cash का परिचय
Tornado Cash एक क्रिप्टोकरेंसी स्मार्ट कॉन्ट्रैक्ट मिक्सर है जो यूज़र्स को एक एड्रेस से क्रिप्टो जमा (deposit) करने और दूसरे वॉलेट से निकालने (withdraw) की सुविधा देता है, बिना उन दो एड्रेस के बीच कोई ट्रैक करने योग्य लिंक बनाए।
Tornado Cash शायद सबसे प्रतिष्ठित zero knowledge स्मार्ट कॉन्ट्रैक्ट एप्लिकेशन है, इसलिए हम समझाएंगे कि यह इतने निचले स्तर (low level) पर कैसे काम करता है कि कोई प्रोग्रामर इस एप्लिकेशन को फिर से बना सके।
यह मान लिया गया है कि पाठक जानता है कि Merkle Trees कैसे काम करते हैं और क्रिप्टोग्राफ़िक हैश को उलटना (invert) असंभव है। यह भी माना जाता है कि पाठक Solidity में कम से कम एक मज़बूत इंटरमीडिएट स्तर का है (क्योंकि हम सोर्स कोड के स्निपेट्स पढ़ेंगे)।
Tornado Cash एक काफ़ी एडवांस स्मार्ट कॉन्ट्रैक्ट है, इसलिए यदि आप अभी भी इस भाषा में नए हैं तो पहले हमारा Solidity Tutorial देखें।
Tornado Cash के बारे में एक त्वरित चेतावनी
कानूनी मुद्दे (Legal issues)
Tornado Cash पर वर्तमान में अमेरिकी सरकार द्वारा प्रतिबंध (sanction) लगाया गया है, इसके साथ इंटरैक्ट करने से आपका वॉलेट “दागदार (taint)” हो सकता है और केंद्रीकृत एक्सचेंजों (centralized exchanges) के साथ इंटरैक्ट करते समय इसके ट्रांज़ैक्शन बाद में फ़्लैग किए जा सकते हैं।
2024 के लिए अपडेट: 28 नवंबर, 2024 तक अमेरिकी अपील न्यायालय द्वारा प्रतिबंध हटा दिया गया है।
Tornado Cash हैक
27 मई को, Tornado Cash governance smart contracts (वे स्मार्ट कॉन्ट्रैक्ट नहीं जिनकी हम यहां समीक्षा करेंगे) हैक हो गए थे जब एक हमलावर ने एक दुर्भावनापूर्ण प्रस्ताव (malicious proposal) पारित किया जिसने उन्हें गवर्नेंस के लिए अधिकांश ERC20 वोटिंग टोकन ERC20 voting tokens for governance दे दिए। उन्होंने तब से अपने लिए एक अपेक्षाकृत छोटी राशि रखते हुए नियंत्रण वापस सौंप दिया है।
Zero knowledge कैसे काम करता है (उन प्रोग्रामर्स के लिए जिन्हें गणित पसंद नहीं है)
आप zero knowledge proofs एल्गोरिदम जाने बिना भी समझ सकते हैं कि Tornado Cash कैसे काम करता है, लेकिन आपको यह पता होना चाहिए कि zero knowledge proofs कैसे काम करते हैं। उनके नाम और कई लोकप्रिय उदाहरणों के विपरीत, Zero Knowledge Proofs किसी गणना (computation) की वैधता साबित करते हैं, न कि किसी निश्चित तथ्य का ज्ञान। वे गणना नहीं करते हैं। वे एक सहमत गणना, गणना किए जाने का एक प्रमाण (proof), और गणना का परिणाम लेते हैं, फिर यह निर्धारित करते हैं कि क्या प्रूवर (prover) ने वास्तव में गणना चलाई और आउटपुट तैयार किया। Zero knowledge हिस्सा इस वैकल्पिक विशेषता से आता है कि गणना-का-प्रमाण (proof-of-computation) इस तरह से दर्शाया जा सकता है जो इनपुट के बारे में कोई जानकारी नहीं देता है।
उदाहरण के लिए, आप RSA algorithm का उपयोग करके किसी संख्या के अभाज्य गुणनखंडों (prime factors) को प्रकट किए बिना प्रदर्शित कर सकते हैं कि आप उन्हें जानते हैं। RSA अलग-थलग रूप से यह दावा नहीं करता है कि “मैं दो गुप्त संख्याएं जानता हूं”; यह साबित करता है कि आपने दो छिपी हुई संख्याओं को एक साथ गुणा किया और सार्वजनिक संख्या (public number) उत्पन्न की। RSA Encryption वास्तव में zero knowledge proofs का एक विशेष मामला है। RSA में, आप प्रदर्शित करते हैं कि आप दो गुप्त अभाज्य (secret primes) जानते हैं जो आपकी पब्लिक की (public key) उत्पन्न करने के लिए एक साथ गुणा होते हैं। Zero Knowledge में, आप उस “जादुई गुणा (magic multiplication)” को मनमाने अंकगणितीय (arbitrary arithmetic) और बूलियन ऑपरेशन्स के लिए सामान्यीकृत (generalize) करते हैं। एक बार जब हमारे पास प्राथमिक ऑपरेशन्स के लिए zero knowledge ऑपरेशन्स होते हैं, तो हम हैश फ़ंक्शंस, Merkle roots, या यहां तक कि पूरी तरह से काम करने वाली वर्चुअल मशीन के प्रीइमेज (preimage) जानने जैसी अधिक विदेशी (exotic) चीज़ों को साबित करने के लिए zero knowledge proofs बना सकते हैं।
एक और बात: एक zero knowledge proof वेरिफिकेशन गणना नहीं करता है, यह केवल यह सत्यापित करता है कि किसी ने गणना की और दावा किया गया आउटपुट उत्पन्न किया।
एक और उपयोगी उपप्रमेय (corollary): किसी गणना का zero knowledge proof उत्पन्न करने के लिए, लेकिन सत्यापित करने के लिए नहीं, आपको वास्तव में गणना करनी होगी।
यह बात समझ में आती है न? आप वास्तव में प्रीइमेज को हैश किए बिना कैसे साबित कर सकते हैं कि आप हैश का प्रीइमेज जानते हैं? इसलिए, प्रूवर वास्तव में गणना करता है और प्रमाण (proof) के रूप में जाना जाने वाला कुछ सहायक डेटा (auxiliary data) बनाता है ताकि यह साबित हो सके कि उन्होंने गणना सही ढंग से की है।
जब आप RSA सिग्नेचर को वैलिडेट करते हैं, तो आप किसी और के प्राइवेट की फ़ैक्टर्स को गुणा करके उनकी पब्लिक की नहीं बनाते हैं, यह तो उद्देश्य को ही विफल कर देगा। आप बस सिग्नेचर को वेरीफाई करते हैं और मैसेज एक अलग एल्गोरिदम का उपयोग करके चेक आउट हो जाता है। इसलिए, एक गणना में निम्नलिखित शामिल हैं
verifier_algorithm(proof, computation, public_output) == true
RSA के संदर्भ में, आप पब्लिक की को गणना का परिणाम, और सिग्नेचर तथा मैसेज को zero knowledge proof मान सकते हैं।
गणना और आउटपुट सार्वजनिक हैं। हम सहमत हैं कि हम एक निश्चित हैश फ़ंक्शन का उपयोग कर रहे हैं और हमें एक निश्चित आउटपुट मिला है। प्रमाण (proof) उपयोग किए गए इनपुट को “छिपाता” है और केवल यह साबित करता है कि हैश फ़ंक्शन निष्पादित (executed) किया गया था और उसने आउटपुट तैयार किया।

केवल प्रमाण दिए जाने पर एक वेरिफायर (verifier) public_output की गणना नहीं कर सकता है। वेरिफिकेशन चरण (verification step) गणना नहीं करता है, यह केवल प्रमाण के आधार पर सार्वजनिक आउटपुट उत्पन्न करने वाली दावा की गई गणना को सत्यापित करता है।
हम इस लेख में zero knowledge एल्गोरिदम नहीं सिखाने जा रहे हैं, लेकिन जब तक आप यह स्वीकार कर सकते हैं कि हम स्वयं गणना किए बिना यह साबित कर सकते हैं कि गणना हुई है, हम आगे बढ़ने के लिए तैयार हैं।
अनाम क्रिप्टोकरेंसी ट्रांसफ़र कैसे काम करते हैं: मिक्सिंग (mixing)
मौलिक रूप से, गुमनामी (anonymization) के लिए Tornado Cash की रणनीति मिक्सिंग है, जो Monero जैसी अन्य अनाम क्रिप्टोकरेंसी के समान है। कई यूज़र्स एक एड्रेस पर अपनी क्रिप्टोकरेंसी जमा करते हैं, अपने डिपाजिट को एक साथ “मिक्स” करते हैं। फिर वे इस तरह से निकालते हैं कि डिपाजिटर (depositor) और विथड्रॉअर (withdrawer) को एक साथ नहीं जोड़ा जा सकता।
कल्पना करें कि 100 लोग एक डॉलर के बिलों को एक ढेर में रखते हैं। फिर 100 अलग-अलग लोग एक दिन बाद आते हैं और प्रत्येक एक डॉलर का बिल निकालता है। इस योजना में, हमें पता नहीं है कि मूल डिपाजिटर किसको पैसे भेजने की कोशिश कर रहे थे।

एक स्पष्ट समस्या यह है कि यदि कोई भी ढेर से नकदी निकाल सकता है, तो वह जल्दी ही चोरी हो जाएगी। लेकिन अगर हम पीछे कुछ मेटाडेटा छोड़ने की कोशिश करते हैं कि किसे निकालने की अनुमति है, तो यह बता देगा कि डिपाजिटर किसको पैसे भेजने की कोशिश कर रहा है।
मिक्सिंग कभी भी पूरी तरह से निजी (private) नहीं हो सकती
जब आप Tornado Cash को Ether भेजते हैं, तो यह पूरी तरह से सार्वजनिक होता है। जब आप Tornado Cash से निकालते हैं, तो यह भी पूरी तरह से सार्वजनिक होता है। जो बात सार्वजनिक नहीं है वह यह है कि शामिल दो एड्रेस एक-दूसरे से जुड़े हुए हैं (यह मानते हुए कि पर्याप्त अन्य डिपाजिटर्स और विथड्रॉअर्स हैं)।
लोग किसी एड्रेस के बारे में केवल यही बता सकते हैं कि “इस एड्रेस को अपना Ether Tornado Cash से मिला” या “इस दूसरे एड्रेस ने Tornado Cash में डिपाजिट किया।” जब कोई एड्रेस Tornado Cash से निकालता है, तो लोग यह नहीं बता सकते कि क्रिप्टो किस डिपाजिटर से आया है।
Zero knowledge के बिना Tornado Cash: हैश प्रीइमेज प्रूफ़्स के बहुत सारे लॉजिकल ORs
प्राइवेसी की चिंता किए बिना इस समस्या को हल करने का प्रयास करते हैं।
डिपाजिटर दो गुप्त संख्याएँ (secret numbers) बनाता है, उन्हें कंकेटिनेट (concatenate) करता है, और ETH डिपाजिट करते समय इसका हैश ऑन-चेन डाल देता है (हम बाद में चर्चा करेंगे कि हमने एक के बजाय दो गुप्त संख्याएँ क्यों बनाईं)। जब कई लोग डिपाजिट करते हैं, तो एक स्मार्ट कॉन्ट्रैक्ट में कई सार्वजनिक हैश बैठे होते हैं जिनका प्रीइमेज हमें नहीं पता होता है।
विथड्रॉअर आता है, विथड्रॉअर हैश में से एक का प्रीइमेज (दो गुप्त संख्याएं) प्रकट करता है और अपना डिपाजिट निकाल लेता है।
यह स्पष्ट रूप से विफल हो जाता है क्योंकि यह दर्शाता है कि डिपाजिटर ने विथड्रॉअर को ऑफ-चेन गुप्त संख्याओं के बारे में बताया था।
हालाँकि, यदि विथड्रॉअर यह प्रदर्शित कर सकता है कि वे यह प्रकट किए बिना कि यह कौन सा हैश है और हैश के प्रीइमेज को प्रकट किए बिना हैश में से एक का प्रीइमेज जानते हैं, तो हमारे पास एक काम करने वाला क्रिप्टोकरेंसी मिक्सर है!
इसका सीधा (naïve) समाधान एक गणना बनाना है जहाँ हम लूप में हैश के ऊपर जाते हैं:
zkproof_preimage_is_valid(proof, hash_{1}) OR
zkproof_preimage_is_valid(proof, hash_{2}) OR
zkproof_preimage_is_valid(proof, hash_{3}) OR
...
zkproof_preimage_is_valid(proof, hash_{n-1}) OR
zkproof_preimage_is_valid(proof, hash_{n})
याद रखें, वेरिफायर वास्तव में उपरोक्त गणना नहीं कर रहा है, इसलिए हमें नहीं पता कि कौन सा हैश वैध है। वेरिफायर (tornado cash), केवल यह सत्यापित कर रहा है कि प्रूवर ने उपरोक्त गणना की और इसने true वापस किया। इसने true कैसे वापस किया यह अप्रासंगिक है, केवल यह कि इसने किया; और यह केवल तभी true वापस कर सकता है जब प्रूवर किसी एक प्रीइमेज को जानता हो।
इस बिंदु पर यह ध्यान रखना महत्वपूर्ण है: सभी डिपाजिट हैश सार्वजनिक रूप से ज्ञात हैं। जब कोई यूज़र डिपाजिट करता है, तो वे दो गुप्त संख्याओं का हैश सबमिट करते हैं, और वह हैश सार्वजनिक होता है। हम जो छिपाने की कोशिश कर रहे हैं वह यह है कि विथड्रॉअर को किस हैश का प्रीइमेज पता है।
लेकिन यह बहुत बड़ी गणना है। बहुत सारे डिपाजिट के साथ बड़े for-loops महंगे होते हैं। [1]
हमें एक डेटा स्ट्रक्चर की आवश्यकता है जो कॉम्पैक्ट रूप से अपने अंदर बहुत सारे हैश रख सके, और शुक्र है कि हमारे पास है: Merkle Trees।
हैश के एक समूह (bunch of hashes) को स्टोर करने के लिए Merkle trees का उपयोग करना
सभी हैश पर लूप करने के बजाय, हम कह सकते हैं “मैं किसी एक हैश का प्रीइमेज जानता हूँ” और “हैश Merkle Tree के अंदर है।” यह हैश की एक बहुत लंबी एरे की ओर इशारा करने और यह कहने के समान है कि “मैं उन हैश में से एक का प्रीइमेज जानता हूं,” सिवाय इसके कि यह बहुत अधिक कुशल (efficient) है।
Merkle Proofs ट्री के आकार में लॉगरिदमिक (logarithmic) होते हैं, इसलिए इसके लिए बहुत अधिक अतिरिक्त काम की आवश्यकता नहीं होती है (हमारे पहले के विशाल for-loop की तुलना में)।
जब क्रिप्टोकरेंसी डिपाजिट की जाती है, तो यूज़र दो गुप्त संख्याएं जनरेट करता है, उन्हें कंकेटिनेट करता है, उन्हें हैश करता है, और हैश को Merkle tree में डाल देता है।
विथड्रॉ के समय, विथड्रॉअर एक लीफ़ हैश प्रीइमेज (leaf hash preimage) का उत्पादन करता है, फिर यह साबित करता है कि लीफ़ हैश Merkle proof के माध्यम से ट्री में है।
यह निश्चित रूप से डिपाजिटर को विथड्रॉअर से जोड़ता है, लेकिन यदि हम Merkle proof और लीफ़ प्रीइमेज वेरिफिकेशन दोनों zero knowledge तरीके से करते हैं, तो लिंक टूट जाता है!
Zero knowledge proofs हमें यह साबित करने देते हैं कि हमने पब्लिक Merkle root के साथ-साथ लीफ़ के प्रीइमेज के विरुद्ध एक वैध Merkle proof उत्पन्न किया - बिना यह दिखाए कि हमने वह गणना कैसे की।
केवल Merkle proof होने और रूट उत्पन्न करने का zero knowledge proof प्रदान करना पर्याप्त सुरक्षित नहीं है, विथड्रॉअर को यह भी साबित करना होगा कि वे प्रश्न में लीफ़ के प्रीइमेज को जानते हैं।
Merkle Tree के लीव्स (leaves) सभी सार्वजनिक हैं। हर बार जब कोई डिपाजिट करता है, तो वे एक हैश प्रदान करते हैं जो सार्वजनिक रूप से स्टोर किया जाता है। चूंकि Merkle tree पूरी तरह से सार्वजनिक है, इसलिए कोई भी किसी भी लीफ़ के लिए Merkle proof की गणना कर सकता है।
इसलिए, ट्री में लीफ़ होने को साबित करना प्रूफ़ जालसाज़ी (proof forgery) द्वारा चोरी को रोकने के लिए पर्याप्त नहीं है।
विथड्रॉअर को लीफ़ को प्रकट किए बिना, प्रश्न में लीफ़ के प्रीइमेज का ज्ञान भी साबित करना होगा। याद रखें, लीफ़ स्वयं दो संख्याओं का हैश है।
आप देख सकते हैं कि डिपाजिट के लिए फ़ंक्शन में function for deposit _commitment आर्ग्यूमेंट सार्वजनिक है। _commitment वेरिएबल वह लीफ़ है जिसे ट्री में जोड़ा जा रहा है, जो कि दो गुप्त संख्याओं का हैश है, जिसे डिपाजिटर प्रकाशित नहीं करता है।
/**
@dev Deposit funds into the contract. The caller must send (for ETH) or approve (for ERC20) value equal to or `denomination` of this instance.
@param _commitment the note commitment, which is PedersenHash(nullifier + secret)
**/
function deposit(bytes32 _commitment) external payable nonReentrant {
require(!commitments[_commitment], "The commitment has been submitted");
uint32 insertedIndex = _insert(_commitment);
commitments[_commitment] = true;
_processDeposit();
emit Deposit(_commitment, insertedIndex, block.timestamp);
}
प्रभावी रूप से, विथड्रॉ के प्रूफ़ में यह साबित करना शामिल है कि निम्नलिखित गणना की गई है:
processMerkleProof(merkleProof, hash(concat(secret1, secret2))) == root
जहाँ processMerkleProof आर्ग्यूमेंट के रूप में Merkle proof और लीफ़ लेता है, और hash(concat(secret1, secret2)) लीफ़ उत्पन्न करता है।
Tornado Cash के संदर्भ में, वेरिफायर वह Tornado Cash स्मार्ट कॉन्ट्रैक्ट है जो किसी ऐसे व्यक्ति को फ़ंड जारी करेगा जो वैध प्रूफ़ की आपूर्ति करता है।
प्रूवर वह विथड्रॉअर है जो साबित कर सकता है कि उन्होंने लीव्स में से एक का उत्पादन करने के लिए हैश गणना की है। आम तौर पर, एकमात्र व्यक्ति जो विथड्रॉ कर सकता है, वह वही व्यक्ति होता है जिसने डिपाजिट किया था, क्योंकि वे एकमात्र पार्टी होंगे जो साबित कर सकते हैं कि वे हैश प्रीइमेज को जानते हैं। बेशक, इस यूज़र को विथड्रॉ के लिए एक अलग और पूरी तरह से असंबद्ध एड्रेस का उपयोग करना होगा!
विथड्रॉअर वास्तव में उपरोक्त गणना (Merkle proof और लीफ़ हैश जनरेशन) करता है, zk-proof का उत्पादन करता है कि उन्होंने इसे ठीक से पूरा किया है, फिर स्मार्ट कॉन्ट्रैक्ट को इस प्रूफ़ की आपूर्ति करता है।
merkleProof और {secret1, secret2} प्रूफ़ में छिपे हुए हैं, लेकिन गणना के प्रूफ़ के साथ, एक वेरिफायर यह वैलिडेट कर सकता है कि विथड्रॉअर ने लीफ़ और Merkle root को सही ढंग से उत्पन्न करने के लिए वास्तव में गणना चलाई थी।
तो आइए संक्षेप में बताएं:
- डिपाजिटर (The Depositor):
- दो गुप्त संख्याएं जनरेट करता है और उनके कंकेटिनेशन से एक कमिटमेंट हैश बनाता है
- एक कमिटमेंट हैश सबमिट करता है
- Tornado Cash को क्रिप्टो ट्रांसफ़र करता है
- Tornado Cash:
- डिपाजिट चरण (deposit stage) के दौरान:
- Merkle Tree में कमिटमेंट हैश जोड़ता है
- डिपाजिट चरण (deposit stage) के दौरान:
- विथड्रॉअर (The Withdrawer):
- Merkle root के लिए एक वैध Merkle proof जनरेट करता है
- दो गुप्त संख्याओं से कमिटमेंट हैश जनरेट करता है
- उपरोक्त गणनाओं का एक zk-proof जनरेट करता है
- Tornado Cash में प्रूफ़ सबमिट करता है
- Tornado Cash
- विथड्रॉ चरण (withdraw stage) के दौरान:
- Merkle root के विरुद्ध प्रूफ़्स को वेरीफाई करता है
- विथड्रॉअर को क्रिप्टोकरेंसी ट्रांसफ़र करता है
- विथड्रॉ चरण (withdraw stage) के दौरान:
कई बार विथड्रॉ करने (multiple withdrawals) से रोकना
उपरोक्त योजना में एक समस्या है: हमें कई बार विथड्रॉ करने से क्या रोकता है? संभवतः, हमें विथड्रॉ किए गए डिपाजिट का हिसाब रखने के लिए Merkle Tree से लीफ़ को “हटाना” होगा, लेकिन इससे पता चल जाएगा कि कौन सा डिपाजिट हमारा है!
Tornado Cash Merkle Tree से लीव्स को कभी न हटाकर इसे संभालता है। एक बार जब Merkle Tree में एक लीफ़ जोड़ा जाता है, तो वह हमेशा के लिए वहीं रहता है।
कई विथड्रॉअल्स को रोकने के लिए, स्मार्ट कॉन्ट्रैक्ट एक “nullifier scheme” का उपयोग करता है, जो zero-knowledge एप्लिकेशन्स और प्रोटोकॉल में काफी आम है।
Nullifier Scheme
Zero knowledge में एक nullifier scheme एक विदेशी नॉन्स (exotic nonce) की तरह व्यवहार करती है जो गुमनामी की एक परत (layer of anonymity) प्रदान करती है।
यह स्पष्ट हो जाएगा कि एक के बजाय लीफ़ बनाने वाली दो गुप्त संख्याएँ क्यों हैं।
डिपाजिट हैश में जाने वाली दो संख्याएँ nullifier और एक secret होती हैं और लीफ़ उस क्रम में concat(nullifier, secret) का हैश होता है।
विथड्रॉ के दौरान, यूज़र को nullifier का हैश, यानी nullifierHash सबमिट करना होगा और यह प्रूफ़ देना होगा कि उन्होंने nullifier और secret को कंकेटिनेट किया और लीव्स में से एक का उत्पादन करने के लिए उसे हैश किया। स्मार्ट कॉन्ट्रैक्ट तब यह वेरीफाई कर सकता है (zero knowledge एल्गोरिदम का उपयोग करके) कि सेंडर (sender) वास्तव में nullifier hash के प्रीइमेज प्रूफ़ को जानता है।
यह सुनिश्चित करने के लिए कि इसका कभी भी पुन: उपयोग न किया जाए, nullifier hash को मैपिंग (mapping) में जोड़ा जाता है।
यही कारण है कि हमें दो गुप्त संख्याओं की आवश्यकता है। यदि हम दोनों संख्याओं को प्रकट कर देते, तो हमें पता चल जाता कि विथड्रॉअर किस लीफ़ को टारगेट कर रहा था! घटक संख्याओं में से केवल एक को प्रकट करके, यह निर्धारित करना असंभव है कि यह किस लीफ़ से जुड़ा है।
याद रखें, zero knowledge proof वेरिफिकेशन गुप्त इनपुट दिए जाने पर nullifier की गणना नहीं कर सकता है, यह केवल यह सत्यापित कर सकता है कि गणना, आउटपुट और प्रूफ़ संगत (congruent) हैं। यही कारण है कि यूज़र को एक सार्वजनिक nullifierHash और प्रूफ़ सबमिट करना होगा कि उन्होंने इसे छिपे हुए nullifier से गणना की है।
आप इस लॉजिक को नीचे स्क्रीनशॉट किए गए Tornado Cash के withdraw function में देख सकते हैं।

संक्षेप में बताते हैं। यूज़र को साबित करना होगा कि:
- वे लीफ़ का प्रीइमेज जानते हैं
nullifierका पहले कभी उपयोग नहीं किया गया है (यह एक साधारण solidity मैपिंग है, zk वेरिफिकेशन स्टेप नहीं)- वे
nullifier hashऔरnullifierका प्रीइमेज उत्पन्न कर सकते हैं
यहाँ संभावित परिणाम दिए गए हैं:
- यूज़र गलत
nullifierप्रदान करता है:nullifierऔरnullifierप्रीइमेज की जांच के लिए zk proof पास नहीं होगा - यूज़र गलत
secretप्रदान करता है: लीफ़ प्रीइमेज के लिए zk proof पास नहीं होगा - यूज़र गलत
nullifier hashप्रदान करता है (लाइन 86 पर जांच को बायपास करने के लिए):nullifierऔरnullifierप्रीइमेज के लिए zk proof पास नहीं होगा
Incremental Merkle Tree एक गैस कुशल (gas efficient) Merkle Tree है
आपने देखा होगा, हमने उपरोक्त स्पष्टीकरणों में एक चाल (pulled a fast one) चली है। गैस ख़त्म हुए बिना आप ऑन-चेन Merkle Tree को कैसे अपडेट कर सकते हैं? संभवत: बहुत सारे डिपाजिट होते हैं, और पूरी चीज़ की दोबारा गणना करना बहुत महंगा (prohibitive) होगा।
Incremental Merkle Tree कुछ चतुर अनुकूलन (clever optimizations) के साथ इन प्रतिबंधों को दरकिनार कर देता है। लेकिन अनुकूलन पर जाने से पहले, हमें प्रतिबंधों को समझना होगा।
एक incremental Merkle Tree निश्चित गहराई (fixed depth) का एक Merkle Tree है जहाँ प्रत्येक लीफ़ शून्य मान (zero value) के रूप में शुरू होता है, और नॉन-ज़ीरो मान सबसे बाएँ लीफ़ (left-most leaf) से शुरू होकर सबसे दाएँ लीफ़ (right-most leaf) तक एक-एक करके शून्य लीव्स को बदलकर जोड़े जाते हैं।
नीचे दिया गया एनिमेशन 3 की गहराई का एक incremental Merkle Tree प्रदर्शित करता है, जिसमें अधिकतम 8 लीव्स आ सकते हैं। Tornado Cash के लिए हमारी डोमेन शब्दावली के अनुरूप, हम इन्हें वेरिएबल के साथ लेबल किए गए “कमिटमेंट्स (commitments)” कहते हैं।

यहाँ एक incremental Merkle tree की कुछ महत्वपूर्ण विशेषताएं दी गई हैं:
- Merkle Tree की 32 की निश्चित गहराई होती है। इसका मतलब है कि यह
2^32 - 1डिपाजिट्स से अधिक को नहीं संभाल सकता है। (यह Tornado Cash द्वारा चुनी गई एक स्वैच्छिक (arbitrary) गहराई है, लेकिन इसे स्थिर होने की आवश्यकता है)। - Merkle Tree एक ऐसे ट्री के रूप में शुरू होता है जहाँ सभी लीव्स
hash(bytes32(0))होते हैं। - जैसे ही डिपाजिट किए जाते हैं, सबसे बाएं अप्रयुक्त (unused) लीफ़ को कमिटमेंट हैश के साथ अधिलेखित (overwritten) कर दिया जाता है। डिपाजिट्स को लीव्स में “बाएँ से दाएँ” तरीके से जोड़ा जाता है।
- एक बार Merkle Tree में डिपाजिट हो जाने के बाद, इसे हटाया नहीं जा सकता।
- हर नए डिपाजिट के साथ, एक नया रूट स्टोर किया जाता है। Tornado Cash इसे “Merkle Tree with history” कहता है। इसलिए Tornado Cash वास्तव में Merkle roots की एक एरे स्टोर करता है, न कि केवल एक। जाहिर है, जैसे-जैसे सदस्य जोड़े जाते हैं, Merkle root बदलता रहता है।
अब हमारे सामने एक समस्या है: ऑन-चेन 2^32 - 1 लीव्स वाला Merkle Tree बनाने पर गैस ख़त्म हो जाएगी। केवल पहले स्तर की गणना करने के लिए 40 लाख (4 million) से अधिक इटेरेशन्स (iterations) की आवश्यकता होगी, जो स्पष्ट रूप से काम नहीं करेगा।
लेकिन incremental Merkle Trees के प्रतिबंध दो प्रमुख इनवेरिएंट्स (key invariants) को सक्षम करते हैं जो स्मार्ट कॉन्ट्रैक्ट को एक बड़ा कम्प्यूटेशनल शॉर्टकट लेने की अनुमति देते हैं: वर्तमान नोड के दाईं ओर मौजूद हर चीज़ अनुमानित ऊंचाई का एक Merkle सबट्री (Merkle subtrees) है जहाँ सभी रूट शून्य हैं, और वर्तमान नोड के बाईं ओर मौजूद हर चीज़ को फिर से गणना करने के बजाय कैश (cache) किया जा सकता है।
चतुर शॉर्टकट 1: नवीनतम सदस्य (newest member) के दाईं ओर के सभी सबट्री (subtrees) में वे merkle subtrees होते हैं जिनमें सभी लीव्स शून्य होते हैं
सभी शून्य लीव्स वाले Merkle subtrees में अनुमानित (predictable) रूट होते हैं जिनकी पूर्व-गणना (precomputed) की जा सकती है।
चूँकि सभी लीव्स शून्य के रूप में शुरू होते हैं, इसलिए Merkle Tree बनाने में लगने वाले काम के एक महत्वपूर्ण हिस्से में उन Merkle Trees की गणना करना शामिल होगा जहाँ सभी लीव्स शून्य हैं।
नीचे दी गई इमेज देखें और ध्यान दें कि जब सभी लीव्स शून्य होते हैं तो कितनी गणना दोहराई जाती है:

अधिकांश लीफ़-पेयर्स (leaf-pairs) bytes32(0) और bytes32(0) के कंकेटिनेशन होने वाले हैं। फिर वह हैश सिस्टर सबट्री (sister subtree) के समान हैश के साथ कंकेटिनेट किया जाएगा, और इसी तरह आगे भी।
Tornado Cash शून्य की गहराई (depth-zero) वाले ट्री के हैश (केवल एक bytes32(0) leaf का हैश), शून्य के दो लीव्स वाले सबट्री के रूट, शून्य के चार लीव्स वाले सबट्री के रूट, शून्य के आठ लीव्स वाले सबट्री के रूट और इसी तरह आगे के रूट की पूर्व-गणना करता है।
इसका मतलब है कि हम 0, 1, 2, आदि से लेकर 31 की ऊंचाई वाले Merkle Tree (सभी शून्य लीव्स के साथ) के Merkle root की पूर्व-गणना कर सकते हैं (याद रखें, Merkle Tree की ऊंचाई निश्चित है)।
एक Merkle सबट्री की प्रत्येक संभावित ऊंचाई के लिए जहां लीव्स सभी शून्य हैं, Tornado Cash इसकी पूर्व-गणना करता है। यहां Tornado Cash’s Merkle Tree With History में प्रीकंप्यूटेड सूची दी गई है:
/// @dev provides Zero (Empty) elements for a MiMC MerkleTree. Up to 32 levels
function zeros(uint256 i) public pure returns (bytes32) {
if (i == 0) return bytes32(0x2fe54c60d3acabf3343a35b6eba15db4821b340f76e741e2249685ed4899af6c);
else if (i == 1) return bytes32(0x256a6135777eee2fd26f54b8b7037a25439d5235caee224154186d2b8a52e31d);
else if (i == 2) return bytes32(0x1151949895e82ab19924de92c40a3d6f7bcb60d92b00504b8199613683f0c200);
else if (i == 3) return bytes32(0x20121ee811489ff8d61f09fb89e313f14959a0f28bb428a20dba6b0b068b3bdb);
else if (i == 4) return bytes32(0x0a89ca6ffa14cc462cfedb842c30ed221a50a3d6bf022a6a57dc82ab24c157c9);
else if (i == 5) return bytes32(0x24ca05c2b5cd42e890d6be94c68d0689f4f21c9cec9c0f13fe41d566dfb54959);
else if (i == 6) return bytes32(0x1ccb97c932565a92c60156bdba2d08f3bf1377464e025cee765679e604a7315c);
else if (i == 7) return bytes32(0x19156fbd7d1a8bf5cba8909367de1b624534ebab4f0f79e003bccdd1b182bdb4);
else if (i == 8) return bytes32(0x261af8c1f0912e465744641409f622d466c3920ac6e5ff37e36604cb11dfff80);
else if (i == 9) return bytes32(0x0058459724ff6ca5a1652fcbc3e82b93895cf08e975b19beab3f54c217d1c007);
else if (i == 10) return bytes32(0x1f04ef20dee48d39984d8eabe768a70eafa6310ad20849d4573c3c40c2ad1e30);
else if (i == 11) return bytes32(0x1bea3dec5dab51567ce7e200a30f7ba6d4276aeaa53e2686f962a46c66d511e5);
else if (i == 12) return bytes32(0x0ee0f941e2da4b9e31c3ca97a40d8fa9ce68d97c084177071b3cb46cd3372f0f);
else if (i == 13) return bytes32(0x1ca9503e8935884501bbaf20be14eb4c46b89772c97b96e3b2ebf3a36a948bbd);
else if (i == 14) return bytes32(0x133a80e30697cd55d8f7d4b0965b7be24057ba5dc3da898ee2187232446cb108);
else if (i == 15) return bytes32(0x13e6d8fc88839ed76e182c2a779af5b2c0da9dd18c90427a644f7e148a6253b6);
else if (i == 16) return bytes32(0x1eb16b057a477f4bc8f572ea6bee39561098f78f15bfb3699dcbb7bd8db61854);
else if (i == 17) return bytes32(0x0da2cb16a1ceaabf1c16b838f7a9e3f2a3a3088d9e0a6debaa748114620696ea);
else if (i == 18) return bytes32(0x24a3b3d822420b14b5d8cb6c28a574f01e98ea9e940551d2ebd75cee12649f9d);
else if (i == 19) return bytes32(0x198622acbd783d1b0d9064105b1fc8e4d8889de95c4c519b3f635809fe6afc05);
else if (i == 20) return bytes32(0x29d7ed391256ccc3ea596c86e933b89ff339d25ea8ddced975ae2fe30b5296d4);
else if (i == 21) return bytes32(0x19be59f2f0413ce78c0c3703a3a5451b1d7f39629fa33abd11548a76065b2967);
else if (i == 22) return bytes32(0x1ff3f61797e538b70e619310d33f2a063e7eb59104e112e95738da1254dc3453);
else if (i == 23) return bytes32(0x10c16ae9959cf8358980d9dd9616e48228737310a10e2b6b731c1a548f036c48);
else if (i == 24) return bytes32(0x0ba433a63174a90ac20992e75e3095496812b652685b5e1a2eae0b1bf4e8fcd1);
else if (i == 25) return bytes32(0x019ddb9df2bc98d987d0dfeca9d2b643deafab8f7036562e627c3667266a044c);
else if (i == 26) return bytes32(0x2d3c88b23175c5a5565db928414c66d1912b11acf974b2e644caaac04739ce99);
else if (i == 27) return bytes32(0x2eab55f6ae4e66e32c5189eed5c470840863445760f5ed7e7b69b2a62600f354);
else if (i == 28) return bytes32(0x002df37a2642621802383cf952bf4dd1f32e05433beeb1fd41031fb7eace979d);
else if (i == 29) return bytes32(0x104aeb41435db66c3e62feccc1d6f5d98d0a0ed75d1374db457cf462e3a1f427);
else if (i == 30) return bytes32(0x1f3c6fd858e9a7d4b0d1f38e256a09d81d5a5e3c963987e2d4b814cfab7c6ebb);
else if (i == 31) return bytes32(0x2c7a07d20dff79d01fecedc1134284a8d08436606c93693b67e333f671bf69cc);
else revert("Index out of bounds");
}
Merkle root की गणना करते समय, हमें हमेशा पता होता है कि हम किस “z-level” पर हैं और आसानी से वह प्रीकंप्यूटेड Merkle सबट्री रूट प्राप्त कर सकते हैं जहाँ सभी लीव्स शून्य हैं।
“शून्य रूट्स (zero roots)” के बारे में एक तकनीकी बात
Tornado Cash वास्तव में खाली मान के रूप में hash(bytes32(0)) का उपयोग नहीं करता है, यह hash(“tornado”) का उपयोग करता है। यह एल्गोरिदम को प्रभावित नहीं करता है, क्योंकि यह केवल एक स्थिरांक (constant) है। हालांकि, एक अजीब स्थिरांक के बजाय शून्य होने की धारणा (notion) का उपयोग करके Incremental Merkle Trees पर चर्चा करना आसान है।
चतुर शॉर्टकट 2: नवीनतम सदस्य के बाईं ओर के सभी सबट्री में वे सबट्री होते हैं जिनके रूट्स की पुनर्गणना के बजाय कैश किया जा सकता है
उस मामले पर विचार करें जहां हम दूसरा डिपाजिट जोड़ते हैं। हम पहले ही पहले डिपाजिट के हैश की गणना कर चुके हैं। वह हैश एक मैपिंग में कैश हो जाता है जिसे Tornado Cash filledSubtrees कहता है। एक filledSubtree केवल Merkle Tree में एक सबट्री है जहाँ सभी लीव्स नॉन-ज़ीरो हैं। हम नीचे दिए गए एनिमेशन में इसे fs कहते हैं:

कहने का तात्पर्य यह है कि, जब भी आपको बाईं ओर एक मध्यवर्ती (intermediate) हैश की आवश्यकता होती है, तो यह पहले ही आपके लिए गणना किया जा चुका होता है।
यह अच्छी सुविधा इस प्रतिबंध का एक उपोत्पाद (byproduct) है कि लीव्स को बदला या हटाया नहीं जा सकता। एक बार जब कोई सबट्री शून्य के बजाय कमिटमेंट्स से भर जाता है, तो उसे फिर कभी गणना करने की आवश्यकता नहीं होती है।
अब आइए इसे सामान्य (generalize) करें। हमारे बाईं ओर के नोड को “पहले डिपाजिट” के रूप में देखने के बजाय, कल्पना करें कि वह स्वयं एक सबट्री का रूट है।
सबसे चरम मामले में, उस समय विचार करें जब हम बिल्कुल आखिरी लीफ़ डालते हैं। हमारे ठीक बाईं ओर एक “ट्री” होगा जिसमें दूसरे-से-आखिरी लीफ़ (गहराई 0) शामिल होंगे, उसके बाईं ओर गहराई 1 का सबट्री होगा, उसके बाईं ओर गहराई 2 का सबट्री (4 लीव्स के साथ) होगा, उसके बाईं ओर गहराई 3 का सबट्री (8 लीव्स के साथ) होगा, आदि। सबसे चरम मामले में ऐसे 32 से अधिक ट्री नहीं होंगे।
शॉर्टकट्स को मिलाना
हमारे बाईं ओर सब कुछ एक भरा हुआ सबट्री (filled subtree) है (भले ही यह सिर्फ एक लीफ़ हो) और हमारे दाईं ओर सब कुछ हमेशा एक शून्य लीफ़ या एक सबट्री होता है जिसमें सभी शून्य लीव्स होते हैं। चूंकि बाएँ रूट को कैश किया गया है, और दाएँ रूट की पूर्व-गणना सभी शून्यों की ऊँचाई के सबट्री से की गई है, हम किसी भी स्तर पर किसी भी मध्यवर्ती हैश कंकेटिनेशन की कुशलता से गणना कर सकते हैं, और केवल 32 इटेरेशन्स के साथ ऑन-चेन पर गहराई-32 का Merkle Tree उत्पन्न कर सकते हैं। यह सस्ता नहीं है, लेकिन यह संभव है। यह निश्चित रूप से 40 लाख गणनाओं से बहुत बेहतर है!
बाएँ हैश करें या दाएँ हैश करें (Hash left or hash right)?
लेकिन जैसे ही हम “रूट तक जाने के लिए हैश करते हैं (hash our way to the root)”, हमें कैसे पता चलेगा कि सबट्री के हैश को किस क्रम में कंकेटिनेट करना है?
उदाहरण के लिए, हम एक नया कमिटमेंट हैश लेते हैं और इसे एक लीफ़ के रूप में जोड़ते हैं। हमारे ऊपर वाले नोड में, क्या हम इसे new_commitment | other_value या other_value | new_commitment के रूप में कंकेटिनेट करते हैं?
यहाँ तरकीब (trick) है: प्रत्येक सम (even) अनुक्रमित नोड (indexed node) एक लेफ्ट चाइल्ड (left child) है, और प्रत्येक विषम (odd) अनुक्रमित नोड एक राइट चाइल्ड (right child) है। यह लीफ़ नोड्स और ट्री के प्रत्येक स्तर के लिए सत्य है। आप नीचे दिए गए आरेख में यह पैटर्न देख सकते हैं।

आइए पैटर्न के लिए एक अंतर्ज्ञान (intuition) प्राप्त करें। यदि शून्यवां (zeroth) लीफ़ डाला जा रहा है, तो हम रूट के रास्ते में केवल दाईं ओर हैश करने जा रहे हैं। 0 ÷ 2 शून्य ही रहेगा, और उपरोक्त परिभाषा का उपयोग करते हुए शून्य एक सम संख्या है। चूँकि शून्य सम है, हम हमेशा रूट के रास्ते में दाईं ओर हैश करेंगे।
अब दूसरे चरम मामले को देखते हैं। जब अंतिम लीफ़ डाला जाता है, तो रूट के रास्ते में हमेशा बाईं ओर हैश करना चाहिए। ऊपर जाने के रास्ते में हर नोड विषम है। यह पैटर्न बीच के प्रत्येक नोड के लिए सामान्य हो जाता है; लीफ़ के इंडेक्स (index) से शुरू करना और जैसे-जैसे हम ट्री में ऊपर जाते हैं, बार-बार दो से विभाजित (dividing) करना हमें बताएगा कि हम लेफ्ट चाइल्ड पर हैं या राइट चाइल्ड पर हैं। निम्नलिखित एनिमेशन दिखाता है कि जब हम एक विषम नोड पर होते हैं तो हम बाएँ हैश करते हैं और जब हम सम नोड पर होते हैं तो दाएँ हैश करते हैं:

तो किसी भी स्तर पर, हम जानते हैं कि सिबलिंग (sibling) के संबंध में हैश कहाँ जाता है।
इसलिए, हमें दो प्रकार की जानकारी की आवश्यकता है:
- उस नोड का इंडेक्स जिसे हम इन्सर्ट (insert) कर रहे हैं
- क्या वर्तमान इंडेक्स सम है या विषम
उस जानकारी का उपयोग करते हुए Tornado Cash के सोर्स कोड का स्क्रीनशॉट यहाँ दिया गया है। अभी जोड़े गए नए लीफ़ के आधार पर Merkle root को पुन: उत्पन्न करने के लिए for लूप स्तरों (levels) पर लूप करता है।

संक्षेप में, ऑन-चेन Merkle Root को अपडेट करने के लिए, हम
- एक नए इंडेक्स पर एक लीफ़ जोड़ते हैं, उसे
currentIndexपर सेट करते हैं - एक स्तर ऊपर जाते हैं और
currentIndexकोcurrentIndexको दो से विभाजित करने पर सेट करते हैं। फिर,- यदि
currentIndexodd(विषम) है तोfilledSubtreeके साथ बाएँ हैश करें - यदि
currentIndexeven(सम) है तोprecomputedशून्य-ट्री (zero-tree) के साथ दाएँ हैश करें।
- यदि
यह काफी शानदार है कि इस तरह के एक नॉन-ट्रिवियल एल्गोरिदम को एक छोटे Solidity रिप्रजेंटेशन में कंप्रेस किया जा सकता है।
Tornado Cash पिछले 30 रूट्स को स्टोर करता है, क्योंकि प्रत्येक डिपाजिट के साथ रूट बदलता रहता है
जब भी कोई आइटम डाला जाता है, Merkle root को आवश्यक रूप से बदलना चाहिए। यह एक समस्या पैदा कर सकता है यदि कोई विथड्रॉअर नवीनतम रूट के लिए Merkle proof बनाता है (याद रखें, लीव्स सभी सार्वजनिक रूप से उपलब्ध हैं) लेकिन एक डिपाजिट ट्रांज़ैक्शन पहले आता है और रूट को बदल देता है; तब Merkle proof अब वैध नहीं रहेगा। zk वेरिफायर एल्गोरिदम सुनिश्चित करता है कि Merkle proof का प्रूफ़ रूट के लिए वैध है, इसलिए यदि रूट बदलता है, तो प्रूफ़ चेक आउट नहीं होगा।
विथड्रॉअर को अपना ट्रांज़ैक्शन निकालने का समय देने के लिए, वे पिछले 30 रूट्स तक का संदर्भ ले सकते हैं।
वेरिएबल roots uint256 से bytes32 तक की एक मैपिंग है। जब Merkle proof रूट तक पहुंच जाता है (लूप पूरा हो जाता है) तो इसे roots में स्टोर किया जाता है। currentRootIndex को ROOT_HISTORY_SIZE तक बढ़ाया जाता है, लेकिन एक बार जब यह अधिकतम मान (30) तक पहुँच जाता है तो यह इंडेक्स शून्य में रूट को अधिलेखित कर देता है। इस प्रकार, यह एक निश्चित आकार की कतार (fixed size queue) की तरह व्यवहार करता है। नीचे Tornado Cash के Merkle Tree कोड के _insert फ़ंक्शन का एक स्निपेट दिया गया है। रूट की पुनर्गणना होने के बाद, इसे ऊपर वर्णित तरीके से स्टोर किया जाता है।

Incremental Merkle Tree के लिए स्टोरेज वेरिएबल्स
Merkle Tree with history को काम करने के लिए आवश्यक स्टोरेज वेरिएबल्स यहां दिए गए हैं।
mapping(uint256 => bytes32) public filledSubtrees;
mapping(uint256 => bytes32) public roots;
uint32 public constant ROOT_HISTORY_SIZE = 30;
uint32 public currentRootIndex = 0;
uint32 public nextIndex = 0;
filledSubtreesवे सबट्री हैं जिनकी हम पहले ही गणना कर चुके हैं (यानी सभी लीव्स नॉन-ज़ीरो हैं)rootsपिछले 30 रूट्स हैंcurrentRootIndexrootsमें इंडेक्स करने के लिए 0 से 29 तक की एक संख्या हैnextIndexवर्तमान लीफ़ है जिसे यूज़र द्वारा Deposit कॉल करने पर भरा जाएगा।
पब्लिक deposit() फ़ंक्शन Incremental Merkle Tree को कैसे अपडेट करता है
जब कोई यूज़र tornado cash में deposit कॉल करता है, तो Merkle Tree को अपडेट करने के लिए _insert() को कॉल किया जाता है, और बाद में _processDeposit() को कॉल किया जाता है।
function deposit(bytes32 _commitment) external payable nonReentrant {
require(!commitments[_commitment], "The commitment has been submitted");
uint32 insertedIndex = _insert(_commitment);
commitments[_commitment] = true;
_processDeposit();
emit Deposit(_commitment, insertedIndex, block.timestamp);
}
_processDeposit() सिर्फ यह सुनिश्चित करता है कि डिनोमिनेशन (मूल्यवर्ग) सटीक है (आप किस Tornado Cash इंस्टेंस के साथ इंटरैक्ट करते हैं, उसके आधार पर आप केवल 0.1, 1, या 10 Ether जमा कर सकते हैं)। उस बहुत ही सरल ऑपरेशन का कोड नीचे दिया गया है।
function _processDeposit() internal override {
require(msg.value == denomination, "Please send `mixDenomination` ETH along with transaction");
}
हाइपरऑप्टिमाइज़्ड MiMC हैश
ऑन-चेन Merkle root की गणना करने के लिए, किसी को हैश एल्गोरिदम (स्पष्ट रूप से) का उपयोग करना चाहिए लेकिन Tornado Cash पारंपरिक keccak256 का उपयोग नहीं कर रहा है; यह इसके बजाय MiMC का उपयोग करता है।
ऐसा क्यों है यह थोड़ा दायरे से बाहर (out of scope) है, लेकिन कारण यह है कि zero knowledge proof जनरेशन के लिए कुछ हैश दूसरों की तुलना में गणना के लिहाज से सस्ते (computationally cheap) होते हैं। MiMC को “zk friendly” होने के लिए डिज़ाइन किया गया था लेकिन keccak256 नहीं था।
“Zk friendly” का अर्थ है कि एल्गोरिदम स्वाभाविक रूप से उस तरह मैप करता है जिस तरह zero knowledge proof एल्गोरिदम गणनाओं का प्रतिनिधित्व करते हैं।
लेकिन इससे एक अजीब पहेली (funny conundrum) पैदा होती है कि जब कोई नया नोड जोड़ा जाता है तो रूट की पुनर्गणना करने के लिए MiMC को ऑन-चेन कैलकुलेट किया जाना चाहिए, और Ethereum में zk-friendly हैश के लिए पूर्व-संकलित (precompiled) कॉन्ट्रैक्ट नहीं हैं। (शायद आप इसके लिए एक EIP लिख सकते हैं?)
इसलिए, Tornado Cash टीम ने इसे स्वयं रॉ बाइटकोड (raw bytecode) में लिखा। यदि आप Tornado Cash के लिए Etherscan कॉन्ट्रैक्ट वेरिफिकेशन देखते हैं, तो आपको एक चेतावनी दिखाई देगी:

Etherscan Solidity के रॉ बाइटकोड को सत्यापित नहीं कर सकता, क्योंकि MiMC हैश Solidity में नहीं लिखा गया था।
Tornado Cash टीम ने MiMC Hasher को एक अलग smart contract के रूप में डिप्लॉय किया। MiMC हैश का उपयोग करने के लिए, Merkle Tree Code उस कॉन्ट्रैक्ट पर क्रॉस कॉन्ट्रैक्ट कॉल (cross contract call) करता है। जैसा कि आप नीचे दिए गए कोड में देख सकते हैं, ये static calls हैं, क्योंकि इंटरफ़ेस इसे pure के रूप में परिभाषित करता है, इसलिए Etherscan इसे बिना किसी ट्रांज़ैक्शन हिस्ट्री के दिखाता है।
interface IHasher {
function MiMCSponge(uint256 in_xL, uint256 in_xR) external pure returns (uint256 xL, uint256 xR);
}
हम जानते हैं कि यह ऊपर दिए गए संदर्भ में Tornado Cash के कोड के आधार पर “इंटरफ़ेस (interface)” है। (github link)।
एक circom लाइब्रेरी github issue पर, आप इसका औचित्य (justification) देख सकते हैं कि कोड में असेंबली ब्लॉक (assembly blocks) होने के बावजूद सॉलिडिटी वर्ज़न क्यों नहीं है: डायरेक्ट स्टैक मैनिपुलेशन संभव नहीं है।
(साइडनोट: बहुत निम्न स्तर (low level) के क्रिप्टोग्राफ़ी एल्गोरिदम Huff Language के लिए एक शानदार उपयोग-मामला (usecase) हैं, जिसे आप हमारे Huff Language Puzzles के साथ सीख सकते हैं)।
रॉ बाइटकोड के रूप में अपना स्वयं का हैश फ़ंक्शन डिप्लॉय करना
circomlib js रिपॉजिटरी में रॉ बाइटकोड हैश बनाने के लिए जावास्क्रिप्ट टूलिंग (javascript tooling) शामिल है। यहाँ MiMC और Poseidon Hash जनरेट करने का कोड है।
Tornado Cash से विथड्रॉ करना
आरंभ करने के लिए, यूज़र को updateTree script का उपयोग करके स्थानीय रूप से Merkle Tree का पुनर्निर्माण (reconstruct) करना होगा। यह स्क्रिप्ट सभी प्रासंगिक solidity events को डाउनलोड करेगी और Merkle Tree का पुनर्निर्माण करेगी। फिर, यूज़र Merkle proof और लीफ़ कमिटमेंट प्रीइमेजेस का zero knowledge proof जनरेट करेगा। जैसा कि पहले चर्चा की गई थी, Tornado Cash पिछले 30 Merkle roots को स्टोर करता है, इसलिए यूज़र को अपना प्रूफ़ सबमिट करने के लिए यह पर्याप्त समय होना चाहिए। यदि कोई यूज़र प्रूफ़ बनाता है और बहुत लंबा इंतज़ार करता है, तो उन्हें प्रूफ़ को फिर से बनाना (regenerate) होगा।
tornado cash कॉन्ट्रैक्ट जांच करेगा:
- सबमिट किए गए
nullifierHashका पहले उपयोग नहीं किया गया है - रूट रूट हिस्ट्री में है (पिछले 30 रूट्स)
- zero knowledge proof चेक आउट हो जाता है:
a. छिपा हुआ हैश प्रीइमेज लीफ़ जनरेट करता है
b. यूज़र वास्तव मेंnullifierHashप्रीइमेज जानता है
c. यूज़र ने उस लीफ़ का उपयोग करके एक Merkle proof बनाया जिसके परिणामस्वरूप प्रस्तावित रूट मिलता है।
d. प्रस्तावित रूट पिछले 30 रूट्स में से एक है (इसे Solidity कोड में सार्वजनिक रूप से जांचा जाता है)
यहाँ उपरोक्त चरणों का विज़ुअलाइज़ेशन दिया गया है:

उस समझ के साथ, नीचे दिए गए Tornado Cash के withdraw फ़ंक्शन का कोड स्व-व्याख्यात्मक (self explanatory) होना चाहिए।
function withdraw(bytes calldata _proof,
bytes32 _root,
bytes32 _nullifierHash,
address payable _recipient,
address payable _relayer,
uint256 _fee,
uint256 _refund
) external payable nonReentrant {
require(_fee <= denomination, "Fee exceeds transfer value");
require(!nullifierHashes[_nullifierHash], "The note has been already spent");
require(isKnownRoot(_root), "Cannot find your merkle root"); // Make sure to use a recent onerequire(
verifier.verifyProof(
_proof,
[uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund]
),
"Invalid withdraw proof"
);
nullifierHashes[_nullifierHash] = true;
_processWithdraw(_recipient, _relayer, _fee, _refund);
emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee);
}
उपरोक्त _relayer, _fee, और _refund का संबंध वैकल्पिक ट्रांज़ैक्शन रिलेयर्स (transaction relayers) को फीस का भुगतान करने से है, जिसे हम संक्षेप में समझाएंगे।
फ़ंक्शन isKnownRoot(root) यह मान्य (validate) करता है कि प्रस्तावित रूट पिछले 30 में से एक है
यह एक साधारण do-while लूप है जो यह देखने के लिए वर्तमान इंडेक्स (अंतिम सक्रिय लीफ़) से पीछे की ओर लूप करता है कि withdraw फ़ंक्शन में सबमिट किया गया रूट रूट्स की हिस्ट्री में है या नहीं। (github link)
चूँकि केवल 30 का लुकबैक (lookback) है, इसलिए हमें बहुत अधिक गैस का उपयोग करने वाले अनबाउंडेड लूप्स (unbounded loops) के बारे में चिंता करने की ज़रूरत नहीं है।
/**
@dev Whether the root is present in the root history
*/function isKnownRoot(bytes32 _root) public view returns (bool) {
if (_root == 0) {
return false;
}
uint32 _currentRootIndex = currentRootIndex;
uint32 i = _currentRootIndex;
do {
if (_root == roots[i]) {
return true;
}
if (i == 0) {
i = ROOT_HISTORY_SIZE; // 30
}
i--;
} while (i != _currentRootIndex);
return false;
}
zero knowledge proof गणना को परिभाषित करने के लिए Circom कोड
यह अनुभाग ZK प्रूफ़ को वेरीफाई करने वाले सर्किट बनाने के लिए उपयोग किए गए circom कोड का वर्णन करता है। यदि आप Circom से अपरिचित हैं और इसे सीखना चाहते हैं, तो कृपया Circom में हमारे zero knowledge puzzles देखें।
फिर भी, हम एक उच्च स्तर पर Circom कोड को समझाने का प्रयास करेंगे।
Circom ब्लॉकचेन पर निष्पादित (executed) नहीं होता है, इसे भयानक (terrifying) Solidity कोड में ट्रांसपाइल (transpile) किया जाता है जिसे आप Tornado Cash के Verifier.sol में देख सकते हैं। इसके इतने डरावने दिखने का कारण यह है कि यह वास्तव में zk-proof वेरिफिकेशन के लिए गणित को निष्पादित कर रहा है। शुक्र है, Circom बहुत अधिक पढ़ने योग्य (readable) है।

यहाँ हमारे पास तीन घटक हैं: HashLeftRight जो हैश को जोड़ता है, DualMux (जो केवल MerkleTreeChecker के लिए एक उपयोगिता है) और MerkleTreeChecker। MerkleTreeChecker एक leaf, एक root, और एक proof लेता है। proof के दो भाग होते हैं: pathElements (सिस्टर सबट्री का Merkle root) और pathIndices (और सर्किट के लिए एक संकेतक (indicator) यह जानने के लिए कि उस स्तर के लिए हैश को किस क्रम में कंकेटिनेट करना है)।
अंतिम पंक्ति, root === hashers[levels - 1].hash वह जगह है जहाँ अंततः root को leaf और proof से मेल खाने के लिए निर्धारित किया जाता है।
याद करें कि nullifierHash nullifier का हैश है और commitment nullifier और secret का हैश है। इस गणना का Circom रिप्रजेंटेशन नीचे दिया गया है। यद्यपि इसे पढ़ना कठिन हो सकता है, यह स्पष्ट होना चाहिए कि इनपुट nullifier और secret हैं, और आउटपुट commitment और nullifierHash हैं।

अब हम zero knowledge एल्गोरिदम के मूल में जा सकते हैं:

एक निजी सिग्नल (private signal) का अर्थ है कि इसे पहले के नामकरण (nomenclature) का उपयोग करके “प्रूफ़ में छिपाया (hidden in the proof)” गया है।
उपरोक्त कोड में, कोड वेरीफाई करता है कि nullifier हैश गणना वैध तरीके से की गई थी। फिर कमिटमेंट प्रीइमेज और Merkle proof पहले से MerkleTree Verifier को प्रदान किए जाते हैं।
यदि सब कुछ चेक आउट हो जाता है, तो verifier true वापस कर देगा, यह दर्शाते हुए कि प्रूफ़ वैध है, और अनाम एड्रेस डिपाजिट को निकाल सकता है।
विथड्रॉ के दौरान फ्रंटरनिंग (frontrunning) को रोकना
सुरक्षा शोधकर्ताओं (Security researchers) ने देखा होगा कि Solidity कोड में फ्रंटरनिंग के खिलाफ कोई बचाव नहीं है। जब कोई मेमपूल (mempool) में एक वैध zero knowledge proof सबमिट करता है, तो किसी को प्रूफ़ कॉपी करने और विथड्रॉ एड्रेस को अपने स्वयं के एड्रेस से बदलने से क्या रोकता है?
यह कुछ ऐसा है जिसे हमने सादगी के लिए नज़रअंदाज़ (glossed over) किया था, लेकिन Withdraw.circom फ़ाइल में डमी सिग्नल (dummy signals) शामिल हैं जो प्राप्तकर्ता (recipient) (और रिलेयर्स के लिए आवश्यक अन्य पैरामीटर) का वर्ग (square) करते हैं। इसका मतलब है कि zk-proof को यह भी प्रदर्शित करना होगा कि विथड्रॉअर ने प्राप्तकर्ता के एड्रेस का वर्ग किया और उनके एड्रेस का वर्ग प्राप्त किया (याद रखें, एड्रेस केवल 20 बाइट संख्याएं हैं)। एड्रेसेस के वर्ग और nullifier तथा secret के हैश की गणना करना एक गणना है, इसलिए इसके किसी भी भाग को गलत करने से पूरा प्रूफ़ अमान्य (invalidate) हो जाता है।

एक relayer और fee क्या है?
एक relayer एक ऑफचेन बॉट है जो किसी प्रकार के भुगतान के बदले अन्य यूज़र्स के लिए गैस का भुगतान करता है। Tornado Cash विथड्रॉअर्स आमतौर पर प्राइवेसी बढ़ाने के लिए पूरी तरह से नए एड्रेसेस का उपयोग करना चाहते हैं, लेकिन पूरी तरह से नए एड्रेसेस के पास विथड्रॉ के लिए भुगतान करने के लिए कोई गैस नहीं होती है।
इसे हल करने के लिए, एक विथड्रॉअर Tornado Cash विथड्रॉअल का एक हिस्सा प्राप्त करने के बदले में एक relayer से ट्रांज़ैक्शन करने के लिए कह सकता है।
relayer के विथड्रॉ एड्रेस को भी ऊपर वर्णित फ्रंटरनिंग सुरक्षा का उपयोग करना चाहिए, और इसे ऊपर कोड स्क्रीनशॉट में देखा जा सकता है।
एक deposit() और withdrawal() का सारांश
जब deposit कॉल किया जाता है:
- यूज़र उस क्रिप्टोकरेंसी के साथ
hash(concat(nullifier, secret))सबमिट करता है जिसे वे डिपाजिट कर रहे हैं। - Tornado Cash वैलिडेट करता है कि जमा की गई राशि वह डिनोमिनेशन है जिसे वह स्वीकार करता है।
- Tornado Cash अगले लीफ़ में कमिटमेंट जोड़ता है। लीव्स को कभी नहीं हटाया जाता है।
जब withdraw कॉल किया जाता है:
- यूज़र Tornado Cash द्वारा उत्सर्जित (emitted) घटनाओं (events) के आधार पर Merkle Tree का पुनर्निर्माण करता है।
- यूज़र को
nullifierका हैश (सार्वजनिक रूप से), वह Merkle root जिसके विरुद्ध वे वेरीफाई कर रहे हैं, और एक zk proof कि वेnullifier,secretऔर Merkle proof जानते हैं, प्रदान करना होगा। - Tornado Cash वेरीफाई करता है कि
nullifierका पहले इस्तेमाल नहीं किया गया है। - Tornado Cash वेरीफाई करता है कि प्रस्तावित रूट पिछले 30 रूट्स में से एक है।
- Tornado Cash zero knowledge proof को वेरीफाई करता है।
यूज़र को बिना ज्ञात प्रीइमेज वाला कोई निरर्थक (nonsense) लीफ़ सबमिट करने से रोकने वाला कुछ भी नहीं है। उस स्थिति में, सबमिट की गई क्रिप्टोकरेंसी हमेशा के लिए कॉन्ट्रैक्ट में फंसी (stuck) रहेगी।
स्मार्ट कॉन्ट्रैक्ट आर्किटेक्चर का एक त्वरित अवलोकन (quick overview)
निम्नलिखित स्मार्ट कॉन्ट्रैक्ट हैं जो Tornado Cash बनाते हैं:

Tornado.sol एक एब्स्ट्रेक्ट कॉन्ट्रैक्ट (abstract contract) है जिसे वास्तव में ERC20Tornado.sol या ETHTornado.sol द्वारा कार्यान्वित किया जाता है, यह इस बात पर निर्भर करता है कि डिप्लॉयमेंट ERC20s या ETH के एक निश्चित डिनोमिनेशन को मिक्स करने के लिए है या नहीं। अलग-अलग ETH डिनोमिनेशन और ERC20 टोकन का अपना tornado cash इंस्टेंस होता है।
MerkleTreeWithHistory.sol में _insert() और isKnownRoot() कार्यक्षमता (functionality) शामिल है जिसके बारे में हमने पहले विस्तार से चर्चा की थी।
Verifier.sol Circom सर्किट का Solidity ट्रांसपिलेशन आउटपुट है।
cTornado.sol गवर्नेंस के लिए ERC20 टोकन है, और कोर प्रोटोकॉल का हिस्सा नहीं है।
Tornado Cash गैस दक्षता (gas efficiency) में कहाँ सुधार कर सकता है
Tornado Cash समग्र रूप से बहुत अच्छी तरह से आर्किटेक्ट किया गया है, लेकिन gas optimization के लिए कुछ अवसर हैं।
- Tornado Cash सभी शून्य लीव्स वाले प्रीकंप्यूटेड Merkle सबट्री के लिए एक लीनियर लुकअप (linear lookup) करता है, लेकिन इसे हार्ड-कोडेड बाइनरी सर्च के साथ कम ऑपरेशन्स में किया जा सकता है।
- Tornado Cash अक्सर स्टैक वेरिएबल्स के लिए
uint32का उपयोग करता है; EVM द्वारा की जाने वाली इम्पलिसिट कास्टिंग (implicit casting) से बचने के लिएuint256एक बेहतर विकल्प होगा। - Tornado Cash में कुछ स्थिरांक (constants) हैं जिन्हें अनावश्यक रूप से पब्लिक के रूप में संशोधित किया गया है। पब्लिक स्थिरांक तब तक आवश्यक नहीं हैं जब तक कि कोई स्मार्ट कॉन्ट्रैक्ट उन्हें पढ़ने वाला न हो, और वे स्मार्ट कॉन्ट्रैक्ट के आकार को बढ़ाते हैं।
- प्रीफ़िक्स ऑपरेटर (
++i) पोस्टफ़िक्स ऑपरेटर (i++) की तुलना में अधिक गैस कुशल हैं, और Tornado Cash लॉजिक को प्रभावित किए बिना इस ऑपरेशन को बदल सकता है। nullifierHashesएक पब्लिक मैपिंग है, लेकिन इसे एक पब्लिक व्यू फ़ंक्शनisSpent()के साथ भी लपेटा (wrapped) गया है। यह अनावश्यक (redundant) है।
निष्कर्ष (Conclusion)
और यह लीजिये; हमने Tornado Cash के संपूर्ण कोडबेस का सर्वेक्षण किया है और प्रत्येक वेरिएबल और फ़ंक्शन क्या करता है, इसकी अच्छी समझ विकसित की है। Tornado Cash अपेक्षाकृत छोटे कोडबेस में प्रभावशाली मात्रा में जटिलता पैक करता है। हमने यहां कई नॉन-ट्रिवियल तकनीकों को देखा है जिनमें शामिल हैं:
- प्रीइमेज को दिए बिना हैश प्रीइमेज के ज्ञान को प्रदर्शित करने के लिए zero knowledge proofs का उपयोग करना
- ऑन-चेन incremental Merkle Tree का उपयोग कैसे करें
- रॉ बाइटकोड से कोड किए गए कस्टम हैश फ़ंक्शन का उपयोग कैसे करें
- Nullifier schemes कैसे काम करती हैं
- गुमनाम रूप से कैसे निकालें (withdraw)
- zero knowledge proofs dapps को फ्रंटरन होने से कैसे बचाएं
RareSkills
यह सामग्री हमारे Zero Knowledge Course का हिस्सा है। सामान्य स्मार्ट कॉन्ट्रैक्ट डेवलपमेंट के लिए, पेशेवर Solidity डेवलपर्स के लिए हमारा Solidity Bootcamp देखें। हम कहीं भी उपलब्ध स्मार्ट कॉन्ट्रैक्ट्स के लिए सबसे कठोर और अद्यतित (up-to-date) प्रशिक्षण कार्यक्रम (training program) प्रदान करते हैं।
स्पष्टीकरण नोट्स (Clarifying Notes)
[1] ZK सर्किट से परिचित कोई भी व्यक्ति जानता है कि हम मनमानी लंबाई के for loops नहीं बना सकते हैं। जब इसे बनाया जाता है तो सर्किट को बहुत बड़े एरे को समायोजित (accommodate) करने के लिए बनाया जाना चाहिए। हालाँकि, यह कहना अभी भी सही है कि इतने सारे हैश पर “लूपिंग (looping)” करना कम्प्यूटेशनल रूप से महंगा है, क्योंकि यह एक असंभव रूप से बड़े (infeasibly massive) सर्किट बनाने के बराबर है।
मूल रूप से 27 जून, 2023 को प्रकाशित