অফলাইন ডাটা ইন্টিগ্রিটির জন্য মার্কেল প্রুফ
ভূমিকা
আদর্শভাবে আমরা ইথিরিয়াম স্টোরেজে সবকিছু সংরক্ষণ করতে চাই, যা হাজার হাজার কম্পিউটারে সংরক্ষিত থাকে এবং এর এভেইলএবিলিটি (ডাটা সেন্সর করা যায় না) এবং ইন্টিগ্রিটি (ডাটা অননুমোদিতভাবে পরিবর্তন করা যায় না) অত্যন্ত বেশি, কিন্তু একটি 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-এ থাকে।
মার্কেল রুট তৈরি করা
প্রথমে আমাদের চেইনে মার্কেল রুট প্রদান করতে হবে।
1const ethers = require("ethers")আমরা ethers প্যাকেজ থেকে হ্যাস ফাংশন ব্যবহার করি (opens in a new tab)।
1// র ডেটা যার অখণ্ডতা আমাদের যাচাই করতে হবে। প্রথম দুটি বাইট হ2// লো একজন ব্যবহারকারীর আইডেন্টিফায়ার, এবং শেষ দুটি বাইট হলো টোকেনের পরিমাণ যা3// বর্তমানে ব্যবহারকারীর মালিকানায় আছে।4const dataArray = [5 0x0bad0010, 0x60a70020, 0xbeef0030, 0xdead0040, 0xca110050, 0x0e660060,6 0xface0070, 0xbad00080, 0x060d0091,7]প্রতিটি এন্ট্রিকে একটি একক 256-বিট ইন্টিজারে এনকোড করলে কোডটি JSON ব্যবহারের তুলনায় কম পাঠযোগ্য হয়। তবে, এর মানে হলো কন্ট্রাক্টে ডাটা পুনরুদ্ধার করতে উল্লেখযোগ্যভাবে কম প্রসেসিং প্রয়োজন, তাই গ্যাস খরচ অনেক কম হয়। আপনি অনচেইন-এ JSON পড়তে পারেন (opens in a new tab), তবে এড়ানো গেলে এটি একটি খারাপ ধারণা।
1// হ্যাস ভ্যালুগুলোর অ্যারে, BigInts হিসেবে2const hashArray = dataArrayএই ক্ষেত্রে আমাদের ডাটা শুরু থেকেই 256-বিট মানের, তাই কোনো প্রসেসিংয়ের প্রয়োজন নেই। যদি আমরা স্ট্রিংয়ের মতো আরও জটিল ডাটা স্ট্রাকচার ব্যবহার করি, তবে আমাদের নিশ্চিত করতে হবে যে আমরা প্রথমে ডাটা হ্যাস করে হ্যাসগুলোর একটি অ্যারে পাই। মনে রাখবেন যে এটি এই কারণেও যে ব্যবহারকারীরা অন্য ব্যবহারকারীদের তথ্য জানলে আমাদের কিছু যায় আসে না। অন্যথায় আমাদের এমনভাবে হ্যাস করতে হতো যাতে ব্যবহারকারী 1 ব্যবহারকারী 0 এর মান জানতে না পারে, ব্যবহারকারী 2 ব্যবহারকারী 3 এর মান জানতে না পারে, ইত্যাদি।
1// হ্যাস ফাংশন যে স্ট্রিংটি আশা করে এবং2// অন্য সব জায়গায় আমরা যে BigInt ব্যবহার করি, তার মধ্যে রূপান্তর করুন।3const hash = (x) =>4 BigInt(ethers.utils.keccak256("0x" + x.toString(16).padStart(64, 0)))ethers হ্যাস ফাংশন একটি হেক্সাডেসিমাল নম্বরসহ একটি JavaScript স্ট্রিং পাওয়ার আশা করে, যেমন 0x60A7, এবং একই স্ট্রাকচারের আরেকটি স্ট্রিং দিয়ে রেসপন্স করে। তবে, বাকি কোডের জন্য 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] // ইনপুট ওভাররাইট করা এড়াতে // প্রয়োজন হলে একটি খালি ভ্যালু যোগ করুন (আমাদের সব লিভস // জোড়ায় জোড়ায় থাকতে হবে)6
7 if (inp.length % 2 === 1) inp.push(empty)8
9 for (var i = 0; i < inp.length; i += 2)10 result.push(pairHash(inp[i], inp[i + 1]))11
12 return result13} // oneLevelUpএই ফাংশনটি বর্তমান লেয়ারে মানগুলোর জোড়া হ্যাস করে মার্কেল ট্রিতে এক লেভেল "উপরে ওঠে"। মনে রাখবেন যে এটি সবচেয়ে দক্ষ ইমপ্লিমেন্টেশন নয়, আমরা ইনপুট কপি করা এড়াতে পারতাম এবং লুপে উপযুক্ত সময়ে শুধু hashEmpty যোগ করতে পারতাম, কিন্তু এই কোডটি পড়ার সুবিধার জন্য অপ্টিমাইজ করা হয়েছে।
1const getMerkleRoot = (inputArray) => {2 var result3
4 result = [...inputArray] // ট্রির উপরের দিকে উঠতে থাকুন যতক্ষণ না শুধুমাত্র একটি ভ্যালু থাকে, যা হলো // রুট। // // যদি কোনো লেয়ারে বিজোড় সংখ্যক এন্ট্রি থাকে তবে // oneLevelUp-এর কোড একটি খালি ভ্যালু যোগ করে, তাই যদি আমাদের, উদাহরণস্বরূপ, // ১০টি লিভস থাকে তবে দ্বিতীয় লেয়ারে ৫টি ব্রাঞ্চ, তৃতীয় লেয়ারে ৩টি // ব্রাঞ্চ, চতুর্থ লেয়ারে ২টি এবং পঞ্চমটি হবে রুট5
6 while (result.length > 1) result = oneLevelUp(result)7
8 return result[0]9}রুট পেতে, যতক্ষণ না শুধুমাত্র একটি মান অবশিষ্ট থাকে ততক্ষণ উপরে উঠতে থাকুন।
একটি মার্কেল প্রুফ তৈরি করা
একটি মার্কেল প্রুফ হলো সেই মানগুলো যা মার্কেল রুট ফিরে পেতে প্রমাণ করা মানের সাথে একসাথে হ্যাস করতে হয়। প্রমাণ করার মানটি প্রায়শই অন্যান্য ডাটা থেকে পাওয়া যায়, তাই আমি এটিকে কোডের অংশ হিসেবে না দিয়ে আলাদাভাবে প্রদান করতে পছন্দ করি।
1// একটি মার্কেল প্রুফ এন্ট্রিগুলোর তালিকার ভ্যালু নিয়ে গঠিত হয় যা2// হ্যাস করতে হবে। যেহেতু আমরা একটি সিমেট্রিকাল হ্যাস ফাংশন ব্যবহার করি, তাই আমাদের3// প্রুফ যাচাই করার জন্য আইটেমের লোকেশনের প্রয়োজন নেই, শুধুমাত্র এটি তৈরি করার জন্য প্রয়োজন4const getMerkleProof = (inputArray, n) => {5 var result = [], currentLayer = [...inputArray], currentN = n6
7 // যতক্ষণ না আমরা শীর্ষে পৌঁছাই8 while (currentLayer.length > 1) {9 // কোনো বিজোড় দৈর্ঘ্যের লেয়ার নেই10 if (currentLayer.length % 2)11 currentLayer.push(empty)12
13 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 } // while currentLayer.length > 15
6 return result7} // getMerkleProofঅনচেইন কোড
সবশেষে আমাদের কাছে সেই কোড আছে যা প্রুফ চেক করে। অনচেইন কোডটি Solidity (opens in a new tab)-তে লেখা হয়েছে। এখানে অপ্টিমাইজেশন অনেক বেশি গুরুত্বপূর্ণ কারণ গ্যাস তুলনামূলকভাবে ব্যয়বহুল।
1// SPDX-License-Identifier: Public Domain2pragma solidity ^0.8.0;3
4import "hardhat/console.sol";আমি এটি Hardhat ডেভেলপমেন্ট এনভায়রনমেন্ট (opens in a new tab) ব্যবহার করে লিখেছি, যা আমাদের ডেভেলপ করার সময় Solidity থেকে কনসোল আউটপুট (opens in a new tab) পেতে সাহায্য করে।
1
2contract MerkleProof {3 uint merkleRoot;4
5 function getRoot() public view returns (uint) {6 return merkleRoot;7 }8
9 // অত্যন্ত অনিরাপদ, প্রোডাকশন কোডে এর অ্যাক্সেস10 // এই ফাংশনটি অবশ্যই কঠোরভাবে সীমিত হতে হবে, সম্ভবত একজন11 // মালিকের কাছে12 function setRoot(uint _merkleRoot) external {13 merkleRoot = _merkleRoot;14 } // setRootমার্কেল রুটের জন্য সেট এবং গেট ফাংশন। একটি প্রোডাকশন সিস্টেমে সবাইকে মার্কেল রুট আপডেট করতে দেওয়া একটি অত্যন্ত খারাপ ধারণা। আমি এখানে নমুনা কোডের সরলতার জন্য এটি করেছি। এমন কোনো সিস্টেমে এটি করবেন না যেখানে ডাটা ইন্টিগ্রিটি সত্যিই গুরুত্বপূর্ণ।
1 function hash(uint _a) internal pure returns(uint) {2 return uint(keccak256(abi.encode(_a)));3 }4
5 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;6
7 for(i=0; i<_proof.length; i++) {8 temp = pairHash(temp, _proof[i]);9 }10
11 return temp == merkleRoot;12 }13
14} // 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 গ্যাস।
উদাহরণস্বরূপ Optimism (opens in a new tab)-এর দিকে তাকালে, L1 গ্যাস লেখার খরচ প্রায় 100 gwei এবং L2 গ্যাস খরচ 0.001 gwei (এটি স্বাভাবিক দাম, কনজেশনের সাথে এটি বাড়তে পারে)। তাই একটি L1 গ্যাসের খরচে আমরা L2 প্রসেসিংয়ে এক লক্ষ গ্যাস খরচ করতে পারি। ধরে নিচ্ছি আমরা স্টোরেজ ওভাররাইট করব না, এর মানে হলো আমরা একটি L1 গ্যাসের দামে L2-তে স্টোরেজে প্রায় পাঁচটি শব্দ লিখতে পারি। একটি একক মার্কেল প্রুফের জন্য আমরা সম্পূর্ণ 1024 শব্দ স্টোরেজে লিখতে পারি (ধরে নিচ্ছি যে এগুলো লেনদেন-এ প্রদান করার পরিবর্তে শুরুতেই অনচেইন-এ ক্যালকুলেট করা যেতে পারে) এবং তারপরেও বেশিরভাগ গ্যাস অবশিষ্ট থাকবে।
উপসংহার
বাস্তব জীবনে আপনি হয়তো কখনোই নিজে থেকে মার্কেল ট্রি ইমপ্লিমেন্ট করবেন না। সুপরিচিত এবং অডিটেড লাইব্রেরি রয়েছে যা আপনি ব্যবহার করতে পারেন এবং সাধারণভাবে বলতে গেলে নিজে থেকে ক্রিপ্টোগ্রাফিক প্রিমিটিভগুলো ইমপ্লিমেন্ট না করাই ভালো। তবে আমি আশা করি যে এখন আপনি মার্কেল প্রুফগুলো আরও ভালোভাবে বুঝতে পেরেছেন এবং কখন এগুলো ব্যবহার করা উচিত তা সিদ্ধান্ত নিতে পারবেন।
মনে রাখবেন যে মার্কেল প্রুফগুলো ইন্টিগ্রিটি বজায় রাখলেও, এগুলো এভেইলএবিলিটি বজায় রাখে না। অন্য কেউ আপনার সম্পদ নিতে পারবে না এটা জানা খুব সামান্যই সান্ত্বনা যদি ডাটা স্টোরেজ অ্যাক্সেস না দেওয়ার সিদ্ধান্ত নেয় এবং আপনি সেগুলো অ্যাক্সেস করার জন্য একটি মার্কেল ট্রিও তৈরি করতে না পারেন। তাই মার্কেল ট্রিগুলো কোনো ধরনের ডিসেন্ট্রালাইজড স্টোরেজ, যেমন IPFS-এর সাথে ব্যবহার করা সবচেয়ে ভালো।
আমার আরও কাজের জন্য এখানে দেখুন (opens in a new tab)।
পেজ সর্বশেষ আপডেট: 3 মার্চ, 2026


