অফলাইন ডেটা ইন্টিগ্রিটির জন্য মার্কেল প্রমাণ
ভূমিকা
আদর্শভাবে আমরা ইথেরিয়াম স্টোরেজে সবকিছু সংরক্ষণ করতে চাই, যা হাজার হাজার কম্পিউটারে সংরক্ষিত থাকে এবং এর অত্যন্ত উচ্চ প্রাপ্যতা (ডেটা সেন্সর করা যায় না) এবং ইন্টিগ্রিটি (ডেটা অননুমোদিতভাবে পরিবর্তন করা যায় না) রয়েছে, তবে একটি 32-বাইট শব্দ সংরক্ষণ করতে সাধারণত 20,000 গ্যাস খরচ হয়। আমি যখন এটি লিখছি, তখন সেই খরচ $6.60 এর সমান। প্রতি বাইটে 21 সেন্ট হিসেবে এটি অনেক ব্যবহারের জন্যই খুব ব্যয়বহুল।
এই সমস্যার সমাধানের জন্য ইথেরিয়াম ইকোসিস্টেম বিকেন্দ্রীকৃত উপায়ে ডেটা সংরক্ষণের অনেক বিকল্প উপায় তৈরি করেছে। সাধারণত এগুলোতে প্রাপ্যতা এবং দামের মধ্যে একটি আপস (tradeoff) থাকে। তবে, ইন্টিগ্রিটি সাধারণত নিশ্চিত করা হয়।
এই নিবন্ধে আপনি শিখবেন কীভাবে মার্কেল প্রমাণ (একটি নতুন ট্যাবে খোলে) ব্যবহার করে ব্লকচেইনে ডেটা সংরক্ষণ না করেই ডেটা ইন্টিগ্রিটি নিশ্চিত করা যায়।
এটি কীভাবে কাজ করে?
তাত্ত্বিকভাবে আমরা শুধু অনচেইনে ডেটার হ্যাশ সংরক্ষণ করতে পারি এবং যেসব ট্রানজ্যাকশনে এটি প্রয়োজন সেখানে সমস্ত ডেটা পাঠাতে পারি। তবে, এটি এখনও খুব ব্যয়বহুল। একটি ট্রানজ্যাকশনে এক বাইট ডেটার জন্য প্রায় 16 গ্যাস খরচ হয়, যা বর্তমানে প্রায় আধা সেন্ট, বা প্রতি কিলোবাইটে প্রায় $5। প্রতি মেগাবাইটে $5000 হিসেবে, ডেটা হ্যাশিংয়ের অতিরিক্ত খরচ ছাড়াই এটি অনেক ব্যবহারের জন্য এখনও খুব ব্যয়বহুল।
এর সমাধান হলো ডেটার বিভিন্ন সাবসেট বারবার হ্যাশ করা, যাতে যে ডেটা আপনার পাঠানোর প্রয়োজন নেই তার জন্য আপনি শুধু একটি হ্যাশ পাঠাতে পারেন। আপনি এটি একটি মার্কেল ট্রি ব্যবহার করে করতে পারেন, যা এমন একটি ট্রি ডেটা স্ট্রাকচার যেখানে প্রতিটি নোড তার নিচের নোডগুলোর একটি হ্যাশ:
রুট হ্যাশ হলো একমাত্র অংশ যা অনচেইনে সংরক্ষণ করা প্রয়োজন। একটি নির্দিষ্ট মান প্রমাণ করার জন্য, রুট পেতে এর সাথে যে সমস্ত হ্যাশ যুক্ত করতে হবে তা আপনি প্রদান করেন। উদাহরণস্বরূপ, C প্রমাণ করতে আপনি D, H(A-B) এবং H(E-H) প্রদান করেন।
বাস্তবায়ন
নমুনা কোড এখানে দেওয়া হয়েছে (একটি নতুন ট্যাবে খোলে)।
অফচেইন কোড
এই নিবন্ধে আমরা অফচেইন গণনার জন্য JavaScript ব্যবহার করি। বেশিরভাগ বিকেন্দ্রীকৃত অ্যাপ্লিকেশন (dapp)-এর অফচেইন উপাদান JavaScript-এ থাকে।
মার্কেল রুট তৈরি করা
প্রথমে আমাদের চেইনে মার্কেল রুট প্রদান করতে হবে।
const ethers = require("ethers")
আমরা ethers প্যাকেজ থেকে হ্যাশ ফাংশন ব্যবহার করি (একটি নতুন ট্যাবে খোলে)।
// র ডেটা যার অখণ্ডতা আমাদের যাচাই করতে হবে। প্রথম দুটি বাইট হ
// লো একটি ইউজার আইডেন্টিফায়ার, এবং শেষ দুটি বাইট হলো টোকেনের পরিমাণ যা
// বর্তমানে ইউজারের কাছে আছে।
const dataArray = [
0x0bad0010, 0x60a70020, 0xbeef0030, 0xdead0040, 0xca110050, 0x0e660060,
0xface0070, 0xbad00080, 0x060d0091,
]
প্রতিটি এন্ট্রিকে একটি একক 256-বিট ইন্টিজারে এনকোড করার ফলে উদাহরণস্বরূপ JSON ব্যবহার করার চেয়ে কম পঠনযোগ্য কোড তৈরি হয়। তবে, এর মানে হলো কন্ট্রাক্টে ডেটা পুনরুদ্ধার করতে উল্লেখযোগ্যভাবে কম প্রসেসিং লাগে, তাই গ্যাস খরচ অনেক কম হয়। আপনি অনচেইনে JSON পড়তে পারেন (একটি নতুন ট্যাবে খোলে), তবে এড়ানো গেলে এটি একটি খারাপ ধারণা।
// হ্যাশ ভ্যালুর অ্যারে, BigInt হিসেবে
const hashArray = dataArray
এই ক্ষেত্রে আমাদের ডেটা শুরু থেকেই 256-বিট মান, তাই কোনো প্রসেসিংয়ের প্রয়োজন নেই। যদি আমরা স্ট্রিংয়ের মতো আরও জটিল ডেটা স্ট্রাকচার ব্যবহার করি, তবে আমাদের নিশ্চিত করতে হবে যে আমরা হ্যাশগুলোর একটি অ্যারে পেতে প্রথমে ডেটা হ্যাশ করি। মনে রাখবেন যে এটি এই কারণেও যে ব্যবহারকারীরা অন্য ব্যবহারকারীদের তথ্য জানলে আমাদের কিছু যায় আসে না। অন্যথায় আমাদের এমনভাবে হ্যাশ করতে হতো যাতে ব্যবহারকারী 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 (একটি নতুন ট্যাবে খোলে) 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 (একটি নতুন ট্যাবে খোলে)-তে লেখা হয়েছে। এখানে অপ্টিমাইজেশন অনেক বেশি গুরুত্বপূর্ণ কারণ গ্যাস তুলনামূলকভাবে ব্যয়বহুল।
//SPDX-License-Identifier: Public Domain
pragma solidity ^0.8.0;
import "hardhat/console.sol";
আমি এটি Hardhat ডেভেলপমেন্ট এনভায়রনমেন্ট (একটি নতুন ট্যাবে খোলে) ব্যবহার করে লিখেছি, যা আমাদের ডেভেলপ করার সময় Solidity থেকে কনসোল আউটপুট (একটি নতুন ট্যাবে খোলে) পেতে দেয়।
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 অনুবাদ।
দ্রষ্টব্য: এটি পঠনযোগ্যতার জন্য অপ্টিমাইজেশনের আরেকটি ক্ষেত্র। ফাংশনের সংজ্ঞা (একটি নতুন ট্যাবে খোলে) এর উপর ভিত্তি করে, ডেটাকে একটি bytes32 (একটি নতুন ট্যাবে খোলে) মান হিসেবে সংরক্ষণ করা এবং রূপান্তরগুলো এড়ানো সম্ভব হতে পারে।
// একটি মার্কেল প্রমাণ যাচাই করুন
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 গ্যাস প্রয়োজন।
উদাহরণস্বরূপ অপটিমিজম (একটি নতুন ট্যাবে খোলে)-এর দিকে তাকালে, লেয়ার ১ (l1) গ্যাস লেখার খরচ প্রায় 100 Gwei এবং লেয়ার ২ (l2) গ্যাসের খরচ 0.001 Gwei (এটি স্বাভাবিক দাম, নেটওয়ার্ক ব্যস্ত থাকলে এটি বাড়তে পারে)। তাই একটি লেয়ার ১ (l1) গ্যাসের খরচে আমরা লেয়ার ২ (l2) প্রসেসিংয়ে এক লক্ষ গ্যাস খরচ করতে পারি। ধরে নিচ্ছি যে আমরা স্টোরেজ ওভাররাইট করব না, এর মানে হলো আমরা একটি লেয়ার ১ (l1) গ্যাসের দামে লেয়ার ২ (l2)-এর স্টোরেজে প্রায় পাঁচটি শব্দ লিখতে পারি। একটি একক মার্কেল প্রমাণের জন্য আমরা সম্পূর্ণ 1024 শব্দ স্টোরেজে লিখতে পারি (ধরে নিচ্ছি যে সেগুলো ট্রানজ্যাকশনে প্রদান করার পরিবর্তে শুরুতেই অনচেইনে গণনা করা যেতে পারে) এবং তারপরেও বেশিরভাগ গ্যাস অবশিষ্ট থাকবে।
উপসংহার
বাস্তব জীবনে আপনি হয়তো কখনোই নিজে থেকে মার্কেল ট্রি বাস্তবায়ন করবেন না। সুপরিচিত এবং অডিটেড লাইব্রেরি রয়েছে যা আপনি ব্যবহার করতে পারেন এবং সাধারণভাবে বলতে গেলে নিজে থেকে ক্রিপ্টোগ্রাফিক প্রিমিটিভগুলো বাস্তবায়ন না করাই ভালো। তবে আমি আশা করি যে এখন আপনি মার্কেল প্রমাণ আরও ভালোভাবে বুঝতে পেরেছেন এবং কখন এগুলো ব্যবহার করা উচিত তা সিদ্ধান্ত নিতে পারবেন।
মনে রাখবেন যে মার্কেল প্রমাণ ইন্টিগ্রিটি বজায় রাখলেও, এগুলো প্রাপ্যতা বজায় রাখে না। অন্য কেউ আপনার সম্পদ নিতে পারবে না এটা জানা খুব সামান্যই সান্ত্বনা দেয় যদি ডেটা স্টোরেজ অ্যাক্সেস অস্বীকার করার সিদ্ধান্ত নেয় এবং আপনি সেগুলো অ্যাক্সেস করার জন্য একটি মার্কেল ট্রি-ও তৈরি করতে না পারেন। তাই মার্কেল ট্রি কোনো ধরনের বিকেন্দ্রীকৃত স্টোরেজ, যেমন IPFS-এর সাথে ব্যবহার করা সবচেয়ে ভালো।


