साखळीबाह्य डेटा अखंडतेसाठी मर्केल पुरावे
परिचय
आदर्शपणे आम्हाला सर्व काही इथेरियम स्टोरेजमध्ये संग्रहित करायला आवडेल, जे हजारो संगणकांवर संग्रहित केले जाते आणि ज्याची उपलब्धता अत्यंत उच्च असते (डेटा सेन्सॉर केला जाऊ शकत नाही) आणि अखंडता असते (डेटा अनधिकृत मार्गाने सुधारित केला जाऊ शकत नाही), परंतु 32-byte शब्द संग्रहित करण्यासाठी साधारणपणे 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 वापरतो. बहुतांश विकेंद्रित ॲप्लिकेशन्सचा (dapps) साखळीबाह्य घटक JavaScript मध्ये असतो.
मर्कल रूट तयार करणे
प्रथम आपल्याला चेनला मर्कल रूट प्रदान करणे आवश्यक आहे.
const ethers = require("ethers")
आम्ही ethers पॅकेजमधील हॅश फंक्शन वापरतो (opens in a new tab).
// कच्चा डेटा ज्याच्या अखंडतेची आपल्याला पडताळणी करायची आहे. पहिले दोन बाइट्स
// एक वापरकर्ता ओळखकर्ता आहेत, आणि शेवटचे दोन बाइट्स टोकन्सची रक्कम आहेत जे
// वापरकर्त्याकडे सध्या आहेत.
const dataArray = [
0x0bad0010, 0x60a70020, 0xbeef0030, 0xdead0040, 0xca110050, 0x0e660060,
0xface0070, 0xbad00080, 0x060d0091,
]
प्रत्येक नोंद एकाच 256-bit इंटिजरमध्ये एन्कोड केल्याने, उदाहरणार्थ JSON वापरण्यापेक्षा कमी वाचनीय कोड तयार होतो. तथापि, याचा अर्थ कॉन्ट्रॅक्टमध्ये डेटा पुनर्प्राप्त करण्यासाठी लक्षणीयरीत्या कमी प्रक्रिया करावी लागते, त्यामुळे गॅस खर्च खूप कमी होतो. तुम्ही JSON ऑनचेन वाचू शकता (opens in a new tab), परंतु टाळता येण्यासारखे असल्यास ती एक वाईट कल्पना आहे.
// हॅश मूल्यांचा अॅरे, BigInts म्हणून
const hashArray = dataArray
या प्रकरणात आपला डेटा सुरुवातीलाच 256-bit मूल्यांचा आहे, त्यामुळे कोणत्याही प्रक्रियेची आवश्यकता नाही. जर आपण स्ट्रिंगसारखी अधिक गुंतागुंतीची डेटा रचना वापरली, तर हॅशचा ॲरे मिळवण्यासाठी आपण प्रथम डेटा हॅश केल्याची खात्री करणे आवश्यक आहे. लक्षात घ्या की हे यासाठी देखील आहे कारण वापरकर्त्यांना इतर वापरकर्त्यांची माहिती समजली तरी आपल्याला काही फरक पडत नाही. अन्यथा आपल्याला हॅश करावे लागले असते जेणेकरून वापरकर्ता 1 ला वापरकर्ता 0 चे मूल्य कळणार नाही, वापरकर्ता 2 ला वापरकर्ता 3 चे मूल्य कळणार नाही, इ.
// हॅश फंक्शनला अपेक्षित असलेली स्ट्रिंग आणि
// आपण इतर सर्वत्र वापरत असलेल्या BigInt मध्ये रूपांतरित करा.
const hash = (x) =>
BigInt(ethers.utils.keccak256("0x" + x.toString(16).padStart(64, 0)))
ethers हॅश फंक्शनला 0x60A7 सारख्या हेक्साडेसिमल संख्येसह JavaScript स्ट्रिंग मिळण्याची अपेक्षा असते, आणि ते त्याच रचनेच्या दुसऱ्या स्ट्रिंगसह प्रतिसाद देते. तथापि, उर्वरित कोडसाठी 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 of two) नसते तेव्हा आपल्याला रिकाम्या शाखा हाताळण्याची आवश्यकता असते. हा प्रोग्राम ज्या पद्धतीने हे करतो ते म्हणजे प्लेस होल्डर म्हणून शून्य ठेवणे.
// हॅश अॅरेच्या ट्रीमध्ये एक स्तर वरची गणना करण्यासाठी, हॅश घ्या
// क्रमाने प्रत्येक जोडीचा
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 मधील कोड एक रिकामे मूल्य जोडतो, त्यामुळे जर आपल्याकडे, उदाहरणार्थ, // १० पाने असतील तर दुसऱ्या स्तरावर ५ शाखा, तिसऱ्या स्तरावर ३ // शाखा, चौथ्या स्तरावर २ आणि रूट पाचवे असेल
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]), इ. हॅश करतो. त्यामुळे सम मूल्यांसाठी आपल्याला पुढील मूल्याची आवश्यकता असते, विषम मूल्यांसाठी मागील मूल्याची.
// वरच्या पुढच्या स्तरावर जा
currentN = Math.floor(currentN/2)
currentLayer = oneLevelUp(currentLayer)
} // while currentLayer.length > 1
return result
} // getMerkleProof
ऑनचेन कोड
शेवटी आपल्याकडे असा कोड आहे जो पुरावा तपासतो. ऑनचेन कोड Solidity (opens in a new tab) मध्ये लिहिला आहे. येथे ऑप्टिमायझेशन खूप जास्त महत्त्वाचे आहे कारण गॅस तुलनेने महाग आहे.
//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
मर्कल रूटसाठी सेट आणि गेट फंक्शन्स. प्रॉडक्शन सिस्टीममध्ये प्रत्येकाला मर्कल रूट अपडेट करू देणे ही एक अत्यंत वाईट कल्पना आहे. नमुना कोडच्या साधेपणासाठी मी हे येथे करत आहे. ज्या सिस्टीममध्ये डेटा अखंडता खरोखर महत्त्वाची असते तिथे असे करू नका.
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
गणितीय नोटेशनमध्ये मर्केल पुरावा पडताळणी अशी दिसते: H(proof_n, H(proof_n-1, H(proof_n-2, ... H(proof_1, H(proof_0, value))...))). हा कोड त्याची अंमलबजावणी करतो.
मर्केल पुरावे आणि रोलअप्स एकत्र काम करत नाहीत
मर्केल पुरावे रोलअप्स सोबत चांगले काम करत नाहीत. याचे कारण असे की रोलअप्स सर्व व्यवहार डेटा स्तर १ (l1) वर लिहितात, परंतु स्तर २ (l2) वर प्रक्रिया करतात. व्यवहारासोबत मर्केल पुरावा पाठवण्याचा खर्च प्रति स्तर सरासरी 638 गॅस असतो (सध्या कॉल डेटा मधील एका बाइटचा खर्च 16 गॅस असतो जर तो शून्य नसेल, आणि 4 गॅस असतो जर तो शून्य असेल). जर आपल्याकडे डेटाचे 1024 शब्द असतील, तर मर्केल पुराव्यासाठी दहा स्तर लागतात, किंवा एकूण 6380 गॅस लागतो.
उदाहरणार्थ ऑप्टिमिझम् (opens in a new tab) कडे पाहिल्यास, स्तर १ (l1) गॅस लिहिण्याचा खर्च सुमारे 100 Gwei असतो आणि स्तर २ (l2) गॅसचा खर्च 0.001 Gwei असतो (ही सामान्य किंमत आहे, ती गर्दीनुसार वाढू शकते). त्यामुळे एका स्तर १ (l1) गॅसच्या खर्चात आपण स्तर २ (l2) प्रक्रियेवर एक लाख गॅस खर्च करू शकतो. आपण स्टोरेज ओव्हरराइट करत नाही असे गृहीत धरल्यास, याचा अर्थ असा की आपण एका स्तर १ (l1) गॅसच्या किमतीत स्तर २ (l2) वरील स्टोरेजमध्ये सुमारे पाच शब्द लिहू शकतो. एका मर्केल पुराव्यासाठी आपण संपूर्ण 1024 शब्द स्टोरेजमध्ये लिहू शकतो (ते व्यवहारात प्रदान करण्याऐवजी सुरुवातीलाच ऑनचेन मोजले जाऊ शकतात असे गृहीत धरून) आणि तरीही बहुतांश गॅस शिल्लक राहू शकतो.
निष्कर्ष
खऱ्या आयुष्यात तुम्ही कदाचित कधीही स्वतःहून मर्कल ट्रीजची अंमलबजावणी करणार नाही. तुम्ही वापरू शकता अशा सुप्रसिद्ध आणि ऑडिट केलेल्या लायब्ररीज आहेत आणि सर्वसाधारणपणे सांगायचे तर स्वतःहून क्रिप्टोग्राफिक प्रिमिटिव्ह्जची अंमलबजावणी न करणे सर्वोत्तम आहे. परंतु मला आशा आहे की आता तुम्हाला मर्केल पुरावे अधिक चांगल्या प्रकारे समजले असतील आणि ते कधी वापरणे योग्य आहे हे तुम्ही ठरवू शकाल.
लक्षात घ्या की मर्केल पुरावे अखंडता जपत असले तरी, ते उपलब्धता जपत नाहीत. इतर कोणीही तुमची मालमत्ता घेऊ शकत नाही हे माहित असणे हे एक छोटेसे सांत्वन आहे जर डेटा स्टोरेजने ॲक्सेस नाकारण्याचे ठरवले आणि तुम्ही त्यांना ॲक्सेस करण्यासाठी मर्कल ट्री देखील तयार करू शकत नसाल. त्यामुळे मर्कल ट्रीजचा वापर IPFS सारख्या काही प्रकारच्या विकेंद्रित स्टोरेजसोबत करणे सर्वोत्तम आहे.


