ऑफ़लाइन डेटा अखंडता के लिए मर्कल प्रमाण
परिचय
आदर्श रूप से हम सब कुछ इथेरियम स्टोरेज में स्टोर करना चाहेंगे, जो हजारों कंप्यूटरों में स्टोर होता है और इसकी उपलब्धता (डेटा को सेंसर नहीं किया जा सकता) और अखंडता (डेटा को अनधिकृत तरीके से संशोधित नहीं किया जा सकता) अत्यधिक उच्च होती है, लेकिन 32-बाइट शब्द को स्टोर करने में आमतौर पर 20,000 गैस का खर्च आता है। जब मैं यह लिख रहा हूँ, तो यह लागत $6.60 के बराबर है। 21 सेंट प्रति बाइट के हिसाब से यह कई उपयोगों के लिए बहुत महंगा है।
इस समस्या को हल करने के लिए इथेरियम इकोसिस्टम ने विकेंद्रीकृत तरीके से डेटा स्टोर करने के कई वैकल्पिक तरीके विकसित किए हैं। आमतौर पर इनमें उपलब्धता और कीमत के बीच एक समझौता (ट्रेडऑफ़) शामिल होता है। हालाँकि, अखंडता आमतौर पर सुनिश्चित की जाती है।
इस लेख में आप सीखेंगे कि मर्कल प्रमाण (opens in a new tab) का उपयोग करके ब्लॉकचेन पर डेटा स्टोर किए बिना डेटा अखंडता कैसे सुनिश्चित की जाए।
यह कैसे काम करता है?
सिद्धांत रूप में हम केवल डेटा के हैश को ऑनचेन स्टोर कर सकते हैं, और उन लेन-देन में सारा डेटा भेज सकते हैं जिन्हें इसकी आवश्यकता होती है। हालाँकि, यह अभी भी बहुत महंगा है। एक लेन-देन में डेटा के एक बाइट की लागत लगभग 16 गैस होती है, जो वर्तमान में लगभग आधा सेंट, या लगभग $5 प्रति किलोबाइट है। $5000 प्रति मेगाबाइट पर, यह अभी भी कई उपयोगों के लिए बहुत महंगा है, यहाँ तक कि डेटा की हैशिंग की अतिरिक्त लागत के बिना भी।
इसका समाधान डेटा के विभिन्न सबसेट को बार-बार हैश करना है, ताकि जिस डेटा को आपको भेजने की आवश्यकता नहीं है, उसके लिए आप केवल एक हैश भेज सकें। आप ऐसा मर्कल ट्री का उपयोग करके करते हैं, जो एक ट्री डेटा संरचना है जहाँ प्रत्येक नोड उसके नीचे के नोड्स का हैश होता है:
रूट हैश एकमात्र ऐसा हिस्सा है जिसे ऑनचेन स्टोर करने की आवश्यकता होती है। किसी निश्चित मान को साबित करने के लिए, आप वे सभी हैश प्रदान करते हैं जिन्हें रूट प्राप्त करने के लिए इसके साथ संयोजित करने की आवश्यकता होती है। उदाहरण के लिए, C को साबित करने के लिए आप D, H(A-B), और H(E-H) प्रदान करते हैं।
कार्यान्वयन
नमूना कोड यहाँ प्रदान किया गया है (opens in a new tab)।
ऑफ़चेन कोड
इस लेख में हम ऑफ़चेन गणनाओं के लिए JavaScript का उपयोग करते हैं। अधिकांश विकेंद्रीकृत एप्लिकेशन का ऑफ़चेन घटक JavaScript में होता है।
मर्कल रूट बनाना
सबसे पहले हमें चेन को मर्कल रूट प्रदान करने की आवश्यकता है।
const ethers = require("ethers")
हम ethers पैकेज से हैश फ़ंक्शन का उपयोग करते हैं (opens in a new tab)।
// वह कच्चा डेटा जिसकी अखंडता को हमें सत्यापित करना है। पहले दो बाइट्स ए
// एक उपयोगकर्ता पहचानकर्ता हैं, और अंतिम दो बाइट्स उन टोकन की मात्रा हैं जो
// उपयोगकर्ता के पास वर्तमान में हैं।
const dataArray = [
0x0bad0010, 0x60a70020, 0xbeef0030, 0xdead0040, 0xca110050, 0x0e660060,
0xface0070, 0xbad00080, 0x060d0091,
]
प्रत्येक प्रविष्टि को एकल 256-बिट पूर्णांक में एन्कोड करने से, उदाहरण के लिए, JSON का उपयोग करने की तुलना में कम पठनीय कोड प्राप्त होता है। हालाँकि, इसका मतलब है कि अनुबंध में डेटा प्राप्त करने के लिए काफी कम प्रोसेसिंग करनी पड़ती है, इसलिए गैस की लागत बहुत कम होती है। आप JSON को ऑनचेन पढ़ सकते हैं (opens in a new tab), लेकिन अगर इससे बचा जा सके तो यह एक बुरा विचार है।
// हैश मानों का ऐरे, BigInts के रूप में
const hashArray = dataArray
इस मामले में हमारा डेटा शुरुआत से ही 256-बिट मान है, इसलिए किसी प्रोसेसिंग की आवश्यकता नहीं है। यदि हम स्ट्रिंग्स जैसी अधिक जटिल डेटा संरचना का उपयोग करते हैं, तो हमें यह सुनिश्चित करने की आवश्यकता है कि हम हैश की एक सरणी (array) प्राप्त करने के लिए पहले डेटा को हैश करें। ध्यान दें कि ऐसा इसलिए भी है क्योंकि हमें इस बात की परवाह नहीं है कि उपयोगकर्ता अन्य उपयोगकर्ताओं की जानकारी जानते हैं या नहीं। अन्यथा हमें हैश करना पड़ता ताकि उपयोगकर्ता 1 को उपयोगकर्ता 0 का मान पता न चले, उपयोगकर्ता 2 को उपयोगकर्ता 3 का मान पता न चले, आदि।
// उस स्ट्रिंग के बीच कनवर्ट करें जिसकी हैश फ़ंक्शन अपेक्षा करता है और
// BigInt जिसका हम हर जगह उपयोग करते हैं।
const hash = (x) =>
BigInt(ethers.utils.keccak256("0x" + x.toString(16).padStart(64, 0)))
ethers हैश फ़ंक्शन को हेक्साडेसिमल संख्या के साथ एक JavaScript स्ट्रिंग प्राप्त होने की उम्मीद होती है, जैसे कि 0x60A7, और यह उसी संरचना के साथ एक अन्य स्ट्रिंग के साथ प्रतिक्रिया देता है। हालाँकि, बाकी कोड के लिए BigInt का उपयोग करना आसान है, इसलिए हम इसे हेक्साडेसिमल स्ट्रिंग में बदलते हैं और फिर वापस लाते हैं।
// एक जोड़ी का सममित हैश ताकि हमें परवाह न हो कि क्रम उलट गया है।
const pairHash = (a, b) => hash(hash(a) ^ hash(b))
यह फ़ंक्शन सममित (symmetrical) है (a xor (opens in a new tab) b का हैश)। इसका मतलब है कि जब हम मर्कल प्रमाण की जाँच करते हैं तो हमें इस बात की चिंता करने की आवश्यकता नहीं होती है कि प्रमाण से प्राप्त मान को गणना किए गए मान से पहले रखा जाए या बाद में। मर्कल प्रमाण की जाँच ऑनचेन की जाती है, इसलिए हमें वहाँ जितना कम काम करना पड़े, उतना अच्छा है।
चेतावनी:
क्रिप्टोग्राफी दिखने में जितनी आसान लगती है, उससे कहीं अधिक कठिन है।
इस लेख के प्रारंभिक संस्करण में हैश फ़ंक्शन hash(a^b) था।
यह एक बुरा विचार था क्योंकि इसका मतलब था कि यदि आप a और b के वैध मान जानते थे, तो आप किसी भी वांछित a' मान को साबित करने के लिए b' = a^b^a' का उपयोग कर सकते थे।
इस फ़ंक्शन के साथ आपको b' की गणना इस तरह करनी होगी कि hash(a') ^ hash(b') एक ज्ञात मान (रूट के रास्ते में अगली शाखा) के बराबर हो, जो कि बहुत कठिन है।
// यह दर्शाने के लिए मान कि एक निश्चित शाखा खाली है, इसमें
// कोई मान नहीं है
const empty = 0n
जब मानों की संख्या दो की पूर्णांक घात (integer power) नहीं होती है, तो हमें खाली शाखाओं को संभालना होता है। यह प्रोग्राम इसे प्लेसहोल्डर के रूप में शून्य रखकर करता है।
// हैश ऐरे के ट्री में एक स्तर ऊपर की गणना करें, इसके लिए
// अनुक्रम में प्रत्येक जोड़ी का हैश लें
const oneLevelUp = (inputArray) => {
var result = []
var inp = [...inputArray] // इनपुट को ओवरराइट करने से बचने के लिए // यदि आवश्यक हो तो एक खाली मान जोड़ें (हमें सभी लीव्स को // जोड़े में रखने की आवश्यकता है)
if (inp.length % 2 === 1) inp.push(empty)
for (var i = 0; i < inp.length; i += 2)
result.push(pairHash(inp[i], inp[i + 1]))
return result
} // oneLevelUp
यह फ़ंक्शन वर्तमान लेयर पर मानों के जोड़े को हैश करके मर्कल ट्री में एक स्तर "ऊपर चढ़ता" है। ध्यान दें कि यह सबसे कुशल कार्यान्वयन नहीं है, हम इनपुट की प्रतिलिपि बनाने से बच सकते थे और लूप में उचित होने पर केवल hashEmpty जोड़ सकते थे, लेकिन यह कोड पठनीयता के लिए अनुकूलित किया गया है।
const getMerkleRoot = (inputArray) => {
var result
result = [...inputArray] // ट्री में तब तक ऊपर चढ़ें जब तक कि केवल एक मान न रह जाए, जो कि // रूट है। // // यदि किसी लेयर में प्रविष्टियों की संख्या विषम है तो // oneLevelUp में कोड एक खाली मान जोड़ता है, इसलिए यदि हमारे पास, उदाहरण के लिए, // 10 लीव्स हैं तो हमारे पास दूसरी लेयर में 5 शाखाएं होंगी, तीसरी में 3 // शाखाएं, चौथी में 2 और रूट पांचवीं है
while (result.length > 1) result = oneLevelUp(result)
return result[0]
}
रूट प्राप्त करने के लिए, तब तक ऊपर चढ़ें जब तक कि केवल एक मान न बच जाए।
मर्कल प्रमाण बनाना
मर्कल प्रमाण वे मान हैं जिन्हें मर्कल रूट वापस प्राप्त करने के लिए साबित किए जा रहे मान के साथ हैश किया जाता है। साबित किया जाने वाला मान अक्सर अन्य डेटा से उपलब्ध होता है, इसलिए मैं इसे कोड के हिस्से के बजाय अलग से प्रदान करना पसंद करता हूँ।
// एक मर्कल प्रमाण में उन प्रविष्टियों की सूची का मान होता है जिन्हें
// हैश करना है। क्योंकि हम एक सममित हैश फ़ंक्शन का उपयोग करते हैं, हमें
// प्रमाण को सत्यापित करने के लिए आइटम के स्थान की आवश्यकता नहीं है, केवल इसे बनाने के लिए है
const getMerkleProof = (inputArray, n) => {
var result = [], currentLayer = [...inputArray], currentN = n
// जब तक हम शीर्ष पर नहीं पहुंच जाते
while (currentLayer.length > 1) {
// कोई विषम लंबाई वाली लेयर्स नहीं
if (currentLayer.length % 2)
currentLayer.push(empty)
result.push(currentN % 2
// यदि currentN विषम है, तो प्रमाण में इसके पहले वाले मान के साथ जोड़ें
? currentLayer[currentN-1]
// यदि यह सम है, तो इसके बाद वाला मान जोड़ें
: currentLayer[currentN+1])
हम (v[0],v[1]), (v[2],v[3]), आदि को हैश करते हैं। इसलिए सम (even) मानों के लिए हमें अगले वाले की आवश्यकता होती है, और विषम (odd) मानों के लिए पिछले वाले की।
// अगली लेयर में ऊपर जाएं
currentN = Math.floor(currentN/2)
currentLayer = oneLevelUp(currentLayer)
} // while currentLayer.length > 1
return result
} // getMerkleProof
ऑनचेन कोड
अंत में हमारे पास वह कोड है जो प्रमाण की जाँच करता है। ऑनचेन कोड Solidity (opens in a new tab) में लिखा गया है। यहाँ अनुकूलन (optimization) बहुत अधिक महत्वपूर्ण है क्योंकि गैस अपेक्षाकृत महंगी है।
//SPDX-License-Identifier: Public Domain
pragma solidity ^0.8.0;
import "hardhat/console.sol";
मैंने इसे Hardhat विकास परिवेश (opens in a new tab) का उपयोग करके लिखा है, जो हमें विकास करते समय Solidity से कंसोल आउटपुट (opens in a new tab) प्राप्त करने की अनुमति देता है।
contract MerkleProof {
uint merkleRoot;
function getRoot() public view returns (uint) {
return merkleRoot;
}
// अत्यधिक असुरक्षित, उत्पादन कोड में इस तक पहुंच
// इस फ़ंक्शन को कड़ाई से सीमित किया जाना चाहिए, संभवतः एक
// मालिक तक
function setRoot(uint _merkleRoot) external {
merkleRoot = _merkleRoot;
} // setRoot
मर्कल रूट के लिए सेट और गेट फ़ंक्शन। उत्पादन प्रणाली (production system) में हर किसी को मर्कल रूट अपडेट करने देना एक अत्यंत बुरा विचार है। मैं इसे यहाँ नमूना कोड की सरलता के लिए कर रहा हूँ। इसे उस सिस्टम पर न करें जहाँ डेटा अखंडता वास्तव में मायने रखती है।
function hash(uint _a) internal pure returns(uint) {
return uint(keccak256(abi.encode(_a)));
}
function pairHash(uint _a, uint _b) internal pure returns(uint) {
return hash(hash(_a) ^ hash(_b));
}
यह फ़ंक्शन एक पेयर हैश उत्पन्न करता है। यह केवल hash और pairHash के लिए JavaScript कोड का Solidity अनुवाद है।
नोट: यह पठनीयता के लिए अनुकूलन का एक और मामला है। फ़ंक्शन परिभाषा (opens in a new tab) के आधार पर, डेटा को bytes32 (opens in a new tab) मान के रूप में स्टोर करना और रूपांतरणों से बचना संभव हो सकता है।
// एक मर्कल प्रमाण सत्यापित करें
function verifyProof(uint _value, uint[] calldata _proof)
public view returns (bool) {
uint temp = _value;
uint i;
for(i=0; i<_proof.length; i++) {
temp = pairHash(temp, _proof[i]);
}
return temp == merkleRoot;
}
} // MarkleProof
गणितीय संकेतन (notation) में मर्कल प्रमाण सत्यापन इस तरह दिखता है: H(proof_n, H(proof_n-1, H(proof_n-2, ... H(proof_1, H(proof_0, value))...)))। यह कोड इसे लागू करता है।
मर्कल प्रमाण और रोलअप्स का मेल नहीं होता
मर्कल प्रमाण रोलअप्स के साथ अच्छी तरह से काम नहीं करते हैं। इसका कारण यह है कि रोलअप्स सभी लेन-देन डेटा को लेयर 1 (l1) पर लिखते हैं, लेकिन लेयर 2 (l2) पर प्रोसेस करते हैं। लेन-देन के साथ मर्कल प्रमाण भेजने की लागत औसतन 638 गैस प्रति लेयर होती है (वर्तमान में कॉल डेटा में एक बाइट की लागत 16 गैस होती है यदि यह शून्य नहीं है, और 4 यदि यह शून्य है)। यदि हमारे पास डेटा के 1024 शब्द हैं, तो मर्कल प्रमाण के लिए दस लेयर, या कुल 6380 गैस की आवश्यकता होती है।
उदाहरण के लिए ऑप्टिमिज़्म (opens in a new tab) को देखें, तो लेयर 1 (l1) गैस लिखने की लागत लगभग 100 Gwei है और लेयर 2 (l2) गैस की लागत 0.001 Gwei है (यह सामान्य कीमत है, यह भीड़भाड़ के साथ बढ़ सकती है)। इसलिए एक लेयर 1 (l1) गैस की लागत के लिए हम लेयर 2 (l2) प्रोसेसिंग पर एक लाख गैस खर्च कर सकते हैं। यह मानते हुए कि हम स्टोरेज को ओवरराइट नहीं करते हैं, इसका मतलब है कि हम एक लेयर 1 (l1) गैस की कीमत पर लेयर 2 (l2) पर स्टोरेज में लगभग पाँच शब्द लिख सकते हैं। एक मर्कल प्रमाण के लिए हम पूरे 1024 शब्दों को स्टोरेज में लिख सकते हैं (यह मानते हुए कि उनकी गणना शुरुआत में ऑनचेन की जा सकती है, न कि लेन-देन में प्रदान की जा सकती है) और फिर भी अधिकांश गैस बची रह सकती है।
निष्कर्ष
वास्तविक जीवन में आप शायद कभी भी मर्कल ट्री को स्वयं लागू न करें। ऐसी प्रसिद्ध और ऑडिट की गई लाइब्रेरी हैं जिनका आप उपयोग कर सकते हैं और सामान्य तौर पर यह सबसे अच्छा है कि आप स्वयं क्रिप्टोग्राफ़िक प्रिमिटिव लागू न करें। लेकिन मुझे उम्मीद है कि अब आप मर्कल प्रमाण को बेहतर ढंग से समझते हैं और यह तय कर सकते हैं कि उनका उपयोग करना कब उचित है।
ध्यान दें कि जबकि मर्कल प्रमाण अखंडता को संरक्षित करते हैं, वे उपलब्धता को संरक्षित नहीं करते हैं। यह जानना कि कोई और आपकी संपत्ति नहीं ले सकता है, एक छोटी सी सांत्वना है यदि डेटा स्टोरेज एक्सेस की अनुमति न देने का निर्णय लेता है और आप उन तक पहुँचने के लिए मर्कल ट्री का निर्माण भी नहीं कर सकते हैं। इसलिए मर्कल ट्री का सबसे अच्छा उपयोग किसी प्रकार के विकेंद्रीकृत स्टोरेज, जैसे कि IPFS के साथ किया जाता है।


