ऑफलाइन डेटा अखंडतेसाठी मर्कल प्रुफ्स
प्रस्तावना
आदर्शपणे, आम्ही सर्वकाही इथेरियम स्टोरेजमध्ये संग्रहित करू इच्छितो, जे हजारो संगणकांवर संग्रहित आहे आणि त्यात अत्यंत उच्च उपलब्धता (डेटा सेन्सॉर केला जाऊ शकत नाही) आणि अखंडता (डेटा अनधिकृतपणे सुधारित केला जाऊ शकत नाही) आहे, परंतु ३२-बाईटचा शब्द संग्रहित करण्यासाठी साधारणपणे २०,००० गॅस खर्च येतो. मी हे लिहित असताना, तो खर्च $६.६० च्या बरोबरीचा आहे. प्रति बाइट २१ सेंट्स दराने हे अनेक उपयोगांसाठी खूप महाग आहे.
ही समस्या सोडवण्यासाठी इथेरियम इकोसिस्टमने विकेंद्रित पद्धतीने डेटा संग्रहित करण्याचे अनेक पर्यायी मार्ग विकसित केले आहेत. सहसा त्यात उपलब्धता आणि किंमत यांच्यात तडजोड असते. तथापि, अखंडतेची सहसा खात्री दिली जाते.
या लेखात तुम्ही मर्कल प्रुफ्स (opens in a new tab) वापरून, ब्लॉकचेनवर डेटा संग्रहित न करता डेटाची अखंडता कशी सुनिश्चित करायची हे शिकाल.
हे कसे कार्य करते?
सिद्धांतानुसार, आपण फक्त डेटाचा हॅश ऑनचेन संग्रहित करू शकतो आणि आवश्यक असलेल्या व्यवहारांमध्ये सर्व डेटा पाठवू शकतो. तथापि, हे अजूनही खूप महाग आहे. एका व्यवहारात एका बाइट डेटासाठी सुमारे १६ गॅस खर्च येतो, जे सध्या सुमारे अर्धा सेंट किंवा प्रति किलोबाइट सुमारे $५ आहे. $५००० प्रति मेगाबाइट दराने, डेटा हॅश करण्याच्या अतिरिक्त खर्चाशिवायही, हे अनेक उपयोगांसाठी अजूनही खूप महाग आहे.
यावर उपाय म्हणजे डेटाच्या वेगवेगळ्या उपसंचांना वारंवार हॅश करणे, म्हणजे ज्या डेटाची तुम्हाला पाठवण्याची गरज नाही त्यासाठी तुम्ही फक्त एक हॅश पाठवू शकता. तुम्ही हे मर्कल ट्री वापरून करता, ही एक ट्री डेटा संरचना आहे जिथे प्रत्येक नोड त्याच्या खालील नोड्सचा हॅश असतो:
रूट हॅश हा एकमेव भाग आहे जो ऑनचेन संग्रहित करणे आवश्यक आहे. एखादे विशिष्ट मूल्य सिद्ध करण्यासाठी, तुम्ही ते सर्व हॅश प्रदान करता जे रूट मिळवण्यासाठी त्याच्यासोबत एकत्र करणे आवश्यक आहे. उदाहरणार्थ, C सिद्ध करण्यासाठी तुम्ही D, H(A-B), आणि H(E-H) प्रदान करता.
अंमलबजावणी
नमुना कोड येथे प्रदान केला आहे (opens in a new tab).
ऑफचेन कोड
या लेखात आम्ही ऑफचेन गणनेसाठी JavaScript वापरतो. बहुतेक विकेंद्रित ऍप्लिकेशन्समध्ये त्यांचा ऑफचेन घटक JavaScript मध्ये असतो.
मर्कल रूट तयार करणे
प्रथम, आपल्याला चेनला मर्कल रूट प्रदान करणे आवश्यक आहे.
1const ethers = require("ethers")आम्ही इथर्स पॅकेजमधील हॅश फंक्शन वापरतो (opens in a new tab).
1// कच्चा डेटा ज्याची अखंडता आपल्याला तपासायची आहे. पहिले दोन बाइट्स2// हे एक वापरकर्ता ओळखकर्ता आहेत, आणि शेवटचे दोन बाइट्स सध्या वापरकर्त्याच्या मालकीच्या3// टोकन्सची रक्कम आहेत.4const dataArray = [5 0x0bad0010, 0x60a70020, 0xbeef0030, 0xdead0040, 0xca110050, 0x0e660060,6 0xface0070, 0xbad00080, 0x060d0091,7]प्रत्येक एंट्रीला एकाच २५६-बिट पूर्णांकामध्ये एन्कोड केल्याने, उदाहरणार्थ JSON वापरण्यापेक्षा कमी वाचनीय कोड तयार होतो. तथापि, याचा अर्थ कॉन्ट्रॅक्टमध्ये डेटा पुनर्प्राप्त करण्यासाठी खूपच कमी प्रक्रिया करावी लागते, ज्यामुळे गॅस खर्च खूपच कमी होतो. तुम्ही ऑनचेन JSON वाचू शकता (opens in a new tab), पण टाळता येत असल्यास ही एक वाईट कल्पना आहे.
1// BigInts म्हणून हॅश मूल्यांचा ॲरे2const hashArray = dataArrayया प्रकरणात आमचा डेटा सुरुवातीलाच २५६-बिट मूल्यांचा आहे, त्यामुळे कोणत्याही प्रक्रियेची आवश्यकता नाही. जर आपण स्ट्रिंगसारखी अधिक गुंतागुंतीची डेटा संरचना वापरली, तर हॅशचा ॲरे मिळवण्यासाठी आपल्याला आधी डेटा हॅश करण्याची खात्री करावी लागेल. लक्षात घ्या की हे असे आहे कारण वापरकर्त्यांना इतर वापरकर्त्यांची माहिती कळली तरी आम्हाला काही फरक पडत नाही. अन्यथा आम्हाला हॅश करावे लागले असते जेणेकरून वापरकर्ता १ ला वापरकर्ता ० चे मूल्य कळणार नाही, वापरकर्ता २ ला वापरकर्ता ३ चे मूल्य कळणार नाही, इत्यादी.
1// हॅश फंक्शनला अपेक्षित असलेली स्ट्रिंग आणि आपण इतरत्र वापरत असलेला2// BigInt यांच्यात रूपांतरित करा.3const hash = (x) =>4 BigInt(ethers.utils.keccak256("0x" + x.toString(16).padStart(64, 0)))इथर्स हॅश फंक्शनला हेक्साडेसिमल संख्येसह, जसे की 0x60A7, JavaScript स्ट्रिंग मिळण्याची अपेक्षा असते, आणि ते त्याच संरचनेसह दुसऱ्या स्ट्रिंगसह प्रतिसाद देते. तथापि, उर्वरित कोडसाठी BigInt वापरणे सोपे आहे, म्हणून आम्ही हेक्साडेसिमल स्ट्रिंगमध्ये रूपांतरित करून पुन्हा परत येतो.
1// एका जोडीचा सिमेट्रिकल हॅश जेणेकरून क्रम उलटला तरी आपल्याला फरक पडणार नाही.2const pairHash = (a, b) => hash(hash(a) ^ hash(b))हे फंक्शन सिमेट्रिकल आहे (a xor (opens in a new tab) b चा हॅश). याचा अर्थ असा की जेव्हा आपण मर्कल प्रूफ तपासतो, तेव्हा प्रूफमधील मूल्य गणन केलेल्या मूल्याच्या आधी किंवा नंतर ठेवायचे की नाही याची काळजी करण्याची आपल्याला गरज नाही. मर्कल प्रूफची तपासणी ऑनचेन केली जाते, त्यामुळे तिथे आपल्याला जितके कमी काम करावे लागेल तितके चांगले.
चेतावणी:
क्रिप्टोग्राफी दिसते त्यापेक्षा कठीण आहे.
या लेखाच्या सुरुवातीच्या आवृत्तीत hash(a^b) हे हॅश फंक्शन होते.
ती एक वाईट कल्पना होती कारण याचा अर्थ असा होता की जर तुम्हाला a आणि b ची वैध मूल्ये माहित असतील, तर तुम्ही कोणतेही इच्छित a' मूल्य सिद्ध करण्यासाठी b' = a^b^a' वापरू शकता.
या फंक्शनसह, तुम्हाला b' असे गणन करावे लागेल की hash(a') ^ hash(b') हे एका ज्ञात मूल्याच्या (रूटच्या मार्गावरील पुढील शाखा) बरोबर असेल, जे खूप कठीण आहे.
1// एखादी विशिष्ट शाखा रिकामी आहे, त्यात मूल्य नाही हे दर्शविणारे 2// मूल्य3const empty = 0nजेव्हा मूल्यांची संख्या दोनाचा पूर्णांक घात नसते, तेव्हा आपल्याला रिकाम्या शाखा हाताळण्याची आवश्यकता असते. हा प्रोग्राम हे करण्यासाठी शून्य प्लेसहोल्डर म्हणून ठेवतो.
1// प्रत्येक जोडीचा क्रमाने हॅश घेऊन हॅश ॲरेच्या ट्रीमध्ये एक स्तर वर गणना2// करा3const oneLevelUp = (inputArray) => {4 var result = []5 var inp = [...inputArray] // इनपुट ओव्हरराइट करणे टाळण्यासाठी // आवश्यक असल्यास एक रिक्त मूल्य जोडा (आम्हाला सर्व लीफ्सची // जोडी बनवणे आवश्यक आहे)67 if (inp.length % 2 === 1) inp.push(empty)89 for (var i = 0; i < inp.length; i += 2)10 result.push(pairHash(inp[i], inp[i + 1]))1112 return result13} // वनलेव्हलअपसर्व दाखवाहे फंक्शन चालू लेअरवरील मूल्यांच्या जोड्या हॅश करून मर्कल ट्रीमध्ये एक स्तर "वर चढते". लक्षात घ्या की ही सर्वात कार्यक्षम अंमलबजावणी नाही, आपण इनपुट कॉपी करणे टाळून लूपमध्ये योग्य ठिकाणी hashEmpty जोडू शकलो असतो, परंतु हा कोड वाचनीयतेसाठी ऑप्टिमाइझ केलेला आहे.
1const getMerkleRoot = (inputArray) => {2 var result34 result = [...inputArray] // जोपर्यंत फक्त एक मूल्य शिल्लक राहत नाही तोपर्यंत ट्रीमध्ये वर चढा, तो // रूट आहे. // // जर लेअरमध्ये विषम संख्येने नोंदी असतील तर // oneLevelUp मधील कोड एक रिक्त मूल्य जोडतो, म्हणून जर आपल्याकडे, उदाहरणार्थ, // १० लीफ्स असतील तर दुसऱ्या लेअरमध्ये ५ शाखा, तिसऱ्यात ३ // शाखा, चौथ्यात २ आणि रूट पाचवा असेल56 while (result.length > 1) result = oneLevelUp(result)78 return result[0]9}सर्व दाखवारूट मिळवण्यासाठी, जोपर्यंत फक्त एकच मूल्य शिल्लक राहत नाही तोपर्यंत वर चढा.
मर्कल प्रूफ तयार करणे
मर्कल रूट परत मिळवण्यासाठी, सिद्ध केल्या जात असलेल्या मूल्यासोबत हॅश करावयाची मूल्ये म्हणजे मर्कल प्रूफ. सिद्ध करण्याचे मूल्य अनेकदा इतर डेटावरून उपलब्ध असते, म्हणून मी ते कोडचा भाग म्हणून देण्याऐवजी स्वतंत्रपणे देणे पसंत करतो.
1// मर्कल प्रूफमध्ये ज्या नोंदींच्या सूचीसोबत2// हॅश करायचे आहे त्यांचे मूल्य असते. कारण आपण एक सिमेट्रिकल हॅश फंक्शन वापरतो, आपल्याला प्रूफ सत्यापित करण्यासाठी आयटमच्या स्थानाची3// गरज नाही, फक्त ते तयार करण्यासाठी गरज आहे4const getMerkleProof = (inputArray, n) => {5 var result = [], currentLayer = [...inputArray], currentN = n67 // जोपर्यंत आपण शीर्षावर पोहोचत नाही8 while (currentLayer.length > 1) {9 // विषम लांबीचे लेअर्स नाहीत10 if (currentLayer.length % 2)11 currentLayer.push(empty)1213 result.push(currentN % 214 // जर currentN विषम असेल, तर प्रूफमध्ये त्याच्या आधीचे मूल्य जोडा15 ? currentLayer[currentN-1]16 // जर ते सम असेल, तर त्याच्या नंतरचे मूल्य जोडा17 : currentLayer[currentN+1])18सर्व दाखवाआपण (v[0],v[1]), (v[2],v[3]), इत्यादींना हॅश करतो. म्हणून सम मूल्यांसाठी आपल्याला पुढील मूल्य आणि विषम मूल्यांसाठी मागील मूल्य आवश्यक आहे.
1 // पुढील वरच्या लेअरवर जा2 currentN = Math.floor(currentN/2)3 currentLayer = oneLevelUp(currentLayer)4 } // जोपर्यंत currentLayer.length > 156 return result7} // गेटमर्कलप्रूफऑनचेन कोड
शेवटी, आपल्याकडे प्रूफ तपासणारा कोड आहे. ऑनचेन कोड Solidity (opens in a new tab) मध्ये लिहिलेला आहे. येथे ऑप्टिमायझेशन अधिक महत्त्वाचे आहे कारण गॅस तुलनेने महाग असतो.
1//SPDX-License-Identifier: Public Domain2pragma solidity ^0.8.0;34import "hardhat/console.sol";मी हे Hardhat डेव्हलपमेंट एन्व्हायर्नमेंट (opens in a new tab) वापरून लिहिले आहे, जे आपल्याला डेव्हलपमेंट करताना Solidity मधून कन्सोल आउटपुट (opens in a new tab) मिळविण्याची परवानगी देते.
12contract MerkleProof {3 uint merkleRoot;45 function getRoot() public view returns (uint) {6 return merkleRoot;7 }89 // अत्यंत असुरक्षित, प्रोडक्शन कोडमध्ये या फंक्शनचा ऍक्सेस10 // कठोरपणे मर्यादित असणे आवश्यक आहे, शक्यतो एका11 // मालकापुरता12 function setRoot(uint _merkleRoot) external {13 merkleRoot = _merkleRoot;14 } // सेट रूटसर्व दाखवामर्कल रूटसाठी सेट आणि गेट फंक्शन्स. प्रोडक्शन सिस्टममध्ये प्रत्येकाला मर्कल रूट अपडेट करू देणे ही एक अत्यंत वाईट कल्पना आहे. नमुना कोड सोपा राहावा यासाठी मी येथे असे करत आहे. ज्या सिस्टममध्ये डेटा अखंडता खरोखरच महत्त्वाची आहे तिथे असे करू नका.
1 function hash(uint _a) internal pure returns(uint) {2 return uint(keccak256(abi.encode(_a)));3 }45 function pairHash(uint _a, uint _b) internal pure returns(uint) {6 return hash(hash(_a) ^ hash(_b));7 }हे फंक्शन एक पेअर हॅश तयार करते. हे hash आणि pairHash साठीच्या JavaScript कोडचे फक्त Solidity भाषांतर आहे.
टीप: हे वाचनीयतेसाठी ऑप्टिमायझेशनचे आणखी एक उदाहरण आहे. फंक्शनच्या व्याख्येनुसार (opens in a new tab), डेटा bytes32 (opens in a new tab) मूल्य म्हणून संग्रहित करणे आणि रूपांतरणे टाळणे शक्य होऊ शकते.
1 // एक मर्कल प्रूफ सत्यापित करा2 function verifyProof(uint _value, uint[] calldata _proof)3 public view returns (bool) {4 uint temp = _value;5 uint i;67 for(i=0; i<_proof.length; i++) {8 temp = pairHash(temp, _proof[i]);9 }1011 return temp == merkleRoot;12 }1314} // मार्कलप्रूफसर्व दाखवागणितीय नोटेशनमध्ये मर्कल प्रूफ पडताळणी अशी दिसते: H(proof_n, H(proof_n-1, H(proof_n-2, ... H(proof_1, H(proof_0, value))...)))`. हा कोड त्याची अंमलबजावणी करतो.
मर्कल प्रूफ आणि रोलअप्स एकत्र बसत नाहीत
मर्कल प्रूफ्स रोलअप्स सह चांगले काम करत नाहीत. याचे कारण असे आहे की रोलअप्स सर्व व्यवहार डेटा L1 वर लिहितात, परंतु L2 वर प्रक्रिया करतात. एका व्यवहारासोबत मर्कल प्रूफ पाठवण्याचा खर्च प्रति लेअर सरासरी ६३८ गॅस येतो (सध्या कॉल डेटामधील बाइट शून्य नसल्यास १६ गॅस आणि शून्य असल्यास ४ गॅस खर्च येतो). जर आपल्याकडे १०२४ शब्दांचा डेटा असेल, तर मर्कल प्रूफसाठी दहा लेअर्स लागतात, किंवा एकूण ६३८० गॅस लागतो.
उदाहरणार्थ Optimism (opens in a new tab) कडे पाहिल्यास, L1 गॅस लिहिण्याचा खर्च सुमारे १०० gwei आणि L2 गॅसचा खर्च ०.००१ gwei येतो (ही सामान्य किंमत आहे, गर्दीनुसार ती वाढू शकते). म्हणजे एका L1 गॅसच्या खर्चात आपण L2 प्रक्रियेवर एक लाख गॅस खर्च करू शकतो. आपण स्टोरेज ओव्हरराइट करत नाही असे गृहीत धरल्यास, याचा अर्थ असा की एका L1 गॅसच्या किमतीत आपण L2 वर स्टोरेजमध्ये सुमारे पाच शब्द लिहू शकतो. एकाच मर्कल प्रूफसाठी आपण संपूर्ण १०२४ शब्द स्टोरेजमध्ये लिहू शकतो (सुरुवातीला ते व्यवहारात प्रदान करण्याऐवजी ऑनचेन गणन केले जाऊ शकतात असे गृहीत धरून) आणि तरीही बहुतेक गॅस शिल्लक राहील.
निष्कर्ष
वास्तविक जीवनात तुम्ही कदाचित स्वतःहून मर्कल ट्री कधीही लागू करणार नाही. अशा सुप्रसिद्ध आणि ऑडिट केलेल्या लायब्ररीज आहेत ज्या तुम्ही वापरू शकता आणि सर्वसाधारणपणे, क्रिप्टोग्राफिक प्रिमिटिव्ह स्वतःहून लागू न करणेच उत्तम आहे. पण मला आशा आहे की आता तुम्हाला मर्कल प्रूफ्स अधिक चांगल्या प्रकारे समजले असतील आणि ते केव्हा वापरण्यायोग्य आहेत हे तुम्ही ठरवू शकाल.
लक्षात घ्या की मर्कल प्रूफ्स अखंडता जपतात, पण ते उपलब्धता जपत नाहीत. जर डेटा स्टोरेजने प्रवेश नाकारण्याचा निर्णय घेतला आणि तुम्ही त्यांना ऍक्सेस करण्यासाठी मर्कल ट्री तयार करू शकत नसाल, तर तुमची मालमत्ता इतर कोणीही घेऊ शकत नाही हे जाणून घेणे हे एक छोटेसे सांत्वन आहे. त्यामुळे IPFS सारख्या काही प्रकारच्या विकेंद्रित स्टोरेजसोबत मर्कल ट्री वापरणे उत्तम आहे.
माझ्या कामाबद्दल अधिक माहितीसाठी येथे पहा (opens in a new tab).
पृष्ठ अखेरचे अद्यतन: १८ डिसेंबर, २०२५


