মূল কন্টেন্টে যান
Change page

মার্কেল প্যাট্রিসিয়া ট্রাই

ইথেরিয়াম-এর স্টেট (সমস্ত অ্যাকাউন্ট, ব্যালেন্স এবং স্মার্ট কন্ট্রাক্টের সমষ্টি) একটি বিশেষ ডেটা স্ট্রাকচারে এনকোড করা থাকে, যা কম্পিউটার বিজ্ঞানে সাধারণত মার্কেল ট্রি (Merkle Tree) নামে পরিচিত। এই স্ট্রাকচারটি ক্রিপ্টোগ্রাফির অনেক অ্যাপ্লিকেশনের জন্য দরকারী কারণ এটি ট্রিতে থাকা সমস্ত পৃথক ডেটার মধ্যে একটি যাচাইযোগ্য সম্পর্ক তৈরি করে, যার ফলে একটি একক রুট (root) মান তৈরি হয় যা ডেটা সম্পর্কে বিভিন্ন বিষয় প্রমাণ করতে ব্যবহার করা যেতে পারে।

ইথেরিয়ামের ডেটা স্ট্রাকচার হলো একটি 'সংশোধিত মার্কেল-প্যাট্রিসিয়া ট্রাই', এর এমন নামকরণের কারণ হলো এটি PATRICIA (the Practical Algorithm To Retrieve Information Coded in Alphanumeric)-এর কিছু বৈশিষ্ট্য ধার করে এবং এটি ইথেরিয়াম স্টেটের অন্তর্ভুক্ত আইটেমগুলোর দক্ষ ডেটা রিট্রাইভালের (retrieval) জন্য ডিজাইন করা হয়েছে।

একটি মার্কেল-প্যাট্রিসিয়া ট্রাই ডিটারমিনিস্টিক এবং ক্রিপ্টোগ্রাফিকভাবে যাচাইযোগ্য: একটি স্টেট রুট তৈরি করার একমাত্র উপায় হলো স্টেটের প্রতিটি পৃথক অংশ থেকে এটি গণনা করা, এবং দুটি স্টেট যে অভিন্ন তা রুট হ্যাশ এবং এর পেছনের হ্যাশগুলোর তুলনা করে সহজেই প্রমাণ করা যায় (একটি মার্কেল প্রমাণ)। বিপরীতভাবে, একই রুট হ্যাশ দিয়ে দুটি ভিন্ন স্টেট তৈরি করার কোনো উপায় নেই এবং ভিন্ন মান দিয়ে স্টেট পরিবর্তন করার যেকোনো প্রচেষ্টার ফলে একটি ভিন্ন স্টেট রুট হ্যাশ তৈরি হবে। তাত্ত্বিকভাবে, এই স্ট্রাকচারটি ইনসার্ট, লুকআপ এবং ডিলিট করার জন্য O(log(n)) দক্ষতার 'হোলি গ্রেইল' প্রদান করে।

অদূর ভবিষ্যতে, ইথেরিয়াম একটি ভার্কেল ট্রি (Verkle Tree) স্ট্রাকচারে স্থানান্তরিত হওয়ার পরিকল্পনা করছে, যা ভবিষ্যতের প্রোটোকল উন্নতির জন্য অনেক নতুন সম্ভাবনা উন্মুক্ত করবে।

পূর্বশর্ত

এই পৃষ্ঠাটি আরও ভালোভাবে বোঝার জন্য হ্যাশ (opens in a new tab), মার্কেল ট্রি (opens in a new tab), ট্রাই (opens in a new tab) এবং সিরিয়ালাইজেশন (opens in a new tab) সম্পর্কে প্রাথমিক জ্ঞান থাকা সহায়ক হবে। এই নিবন্ধটি একটি সাধারণ র‍্যাডিক্স ট্রি (radix tree) (opens in a new tab)-এর বর্ণনা দিয়ে শুরু হয়, তারপর ধীরে ধীরে ইথেরিয়ামের আরও অপ্টিমাইজ করা ডেটা স্ট্রাকচারের জন্য প্রয়োজনীয় পরিবর্তনগুলো তুলে ধরে।

সাধারণ র‍্যাডিক্স ট্রাই

একটি সাধারণ র‍্যাডিক্স ট্রাই-তে, প্রতিটি নোড দেখতে নিচের মতো হয়:

[i_0, i_1 ... i_n, value]

যেখানে i_0 ... i_n বর্ণমালার প্রতীকগুলোকে (প্রায়শই বাইনারি বা হেক্স) উপস্থাপন করে, value হলো নোডের টার্মিনাল মান, এবং i_0, i_1 ... i_n স্লটগুলোর মান হয় NULL অথবা অন্য নোডগুলোর পয়েন্টার (আমাদের ক্ষেত্রে, হ্যাশ)। এটি একটি সাধারণ (key, value) স্টোর তৈরি করে।

ধরুন আপনি কী-ভ্যালু পেয়ারের একটি সেটের ওপর একটি ক্রম বজায় রাখার জন্য একটি র‍্যাডিক্স ট্রি ডেটা স্ট্রাকচার ব্যবহার করতে চান। ট্রাই-তে বর্তমানে dog কী-এর সাথে ম্যাপ করা মানটি খুঁজে পেতে, আপনাকে প্রথমে dog-কে বর্ণমালার অক্ষরে রূপান্তর করতে হবে (যা 64 6f 67 দেবে), এবং তারপর মানটি খুঁজে না পাওয়া পর্যন্ত সেই পথ অনুসরণ করে ট্রাই-এর নিচে নামতে হবে। অর্থাৎ, আপনি ট্রাই-এর রুট নোড খুঁজে পেতে একটি ফ্ল্যাট কী/ভ্যালু DB-তে রুট হ্যাশ খোঁজার মাধ্যমে শুরু করবেন। এটি অন্যান্য নোড নির্দেশকারী কী-গুলোর একটি অ্যারে হিসেবে উপস্থাপিত হয়। আপনি সূচক 6-এর মানটিকে একটি কী হিসেবে ব্যবহার করবেন এবং এক স্তর নিচের নোডটি পেতে ফ্ল্যাট কী/ভ্যালু DB-তে এটি খুঁজবেন। তারপর পরবর্তী মানটি খুঁজতে সূচক 4 বেছে নিন, তারপর সূচক 6 বেছে নিন, এবং এভাবেই চলতে থাকবে, যতক্ষণ না আপনি পথটি অনুসরণ করেন: root -> 6 -> 4 -> 6 -> 15 -> 6 -> 7, আপনি নোডের মানটি খুঁজবেন এবং ফলাফলটি রিটার্ন করবেন।

'ট্রাই' এবং অন্তর্নিহিত ফ্ল্যাট কী/ভ্যালু 'DB'-তে কিছু খোঁজার মধ্যে পার্থক্য রয়েছে। তারা উভয়ই কী/ভ্যালু বিন্যাস সংজ্ঞায়িত করে, কিন্তু অন্তর্নিহিত DB একটি কী-এর ঐতিহ্যবাহী 1-ধাপের লুকআপ করতে পারে। ট্রাই-তে একটি কী খোঁজার জন্য উপরে বর্ণিত চূড়ান্ত মানটিতে পৌঁছাতে একাধিক অন্তর্নিহিত DB লুকআপের প্রয়োজন হয়। অস্পষ্টতা দূর করতে চলুন পরেরটিকে path হিসেবে উল্লেখ করি।

র‍্যাডিক্স ট্রাই-এর জন্য আপডেট এবং ডিলিট অপারেশনগুলো নিচের মতো করে সংজ্ঞায়িত করা যেতে পারে:

একটি "মার্কেল" র‍্যাডিক্স ট্রি ডিটারমিনিস্টিকভাবে তৈরি ক্রিপ্টোগ্রাফিক হ্যাশ ডাইজেস্ট ব্যবহার করে নোডগুলোকে যুক্ত করে তৈরি করা হয়। এই কন্টেন্ট-অ্যাড্রেসিং (কী/ভ্যালু DB key == keccak256(rlp(value))-তে) সংরক্ষিত ডেটার একটি ক্রিপ্টোগ্রাফিক অখণ্ডতার গ্যারান্টি প্রদান করে। যদি কোনো নির্দিষ্ট ট্রাই-এর রুট হ্যাশ সর্বজনীনভাবে পরিচিত হয়, তবে অন্তর্নিহিত লিফ ডেটাতে অ্যাক্সেস আছে এমন যে কেউ প্রমাণ করতে পারে যে ট্রাই-টি একটি নির্দিষ্ট পথে একটি নির্দিষ্ট মান অন্তর্ভুক্ত করে, যা ট্রি রুটের সাথে একটি নির্দিষ্ট মান যুক্ত করা প্রতিটি নোডের হ্যাশ প্রদান করে করা যায়।

একজন আক্রমণকারীর পক্ষে এমন একটি (path, value) পেয়ারের প্রমাণ দেওয়া অসম্ভব যা বিদ্যমান নেই, কারণ রুট হ্যাশ শেষ পর্যন্ত এর নিচের সমস্ত হ্যাশের ওপর ভিত্তি করে তৈরি। যেকোনো অন্তর্নিহিত পরিবর্তন রুট হ্যাশকে পরিবর্তন করবে। আপনি হ্যাশটিকে ডেটা সম্পর্কে কাঠামোগত তথ্যের একটি সংকুচিত উপস্থাপনা হিসেবে ভাবতে পারেন, যা হ্যাশিং ফাংশনের প্রি-ইমেজ সুরক্ষা দ্বারা সুরক্ষিত।

আমরা একটি র‍্যাডিক্স ট্রির পারমাণবিক একককে (যেমন, একটি একক হেক্স ক্যারেক্টার, বা 4 বিট বাইনারি সংখ্যা) "নিবল (nibble)" হিসেবে উল্লেখ করব। উপরে বর্ণিত হিসেবে, একবারে একটি নিবল পথ অতিক্রম করার সময়, নোডগুলো সর্বোচ্চ 16টি চাইল্ডকে নির্দেশ করতে পারে তবে একটি value উপাদান অন্তর্ভুক্ত করে। তাই, আমরা সেগুলোকে 17 দৈর্ঘ্যের একটি অ্যারে হিসেবে উপস্থাপন করি। আমরা এই 17-উপাদানের অ্যারেগুলোকে "ব্রাঞ্চ নোড" বলি।

মার্কেল প্যাট্রিসিয়া ট্রাই

র‍্যাডিক্স ট্রাই-এর একটি বড় সীমাবদ্ধতা রয়েছে: এগুলো অদক্ষ। আপনি যদি একটি (path, value) বাইন্ডিং সংরক্ষণ করতে চান যেখানে পথটি, ইথেরিয়ামের মতো, 64 ক্যারেক্টার দীর্ঘ (bytes32-এ নিবলের সংখ্যা), তবে প্রতি ক্যারেক্টারে একটি স্তর সংরক্ষণ করতে আমাদের এক কিলোবাইটের বেশি অতিরিক্ত জায়গার প্রয়োজন হবে এবং প্রতিটি লুকআপ বা ডিলিট করতে পুরো 64টি ধাপ লাগবে। নিচে প্রবর্তিত প্যাট্রিসিয়া ট্রাই এই সমস্যার সমাধান করে।

অপ্টিমাইজেশন

একটি মার্কেল প্যাট্রিসিয়া ট্রাই-এর নোড নিচের যেকোনো একটি হতে পারে:

  1. NULL (ফাঁকা স্ট্রিং হিসেবে উপস্থাপিত)
  2. branch একটি 17-আইটেমের নোড [ v0 ... v15, vt ]
  3. leaf একটি 2-আইটেমের নোড [ encodedPath, value ]
  4. extension একটি 2-আইটেমের নোড [ encodedPath, key ]

64 ক্যারেক্টারের পথের ক্ষেত্রে এটি অনিবার্য যে ট্রাই-এর প্রথম কয়েকটি স্তর অতিক্রম করার পরে, আপনি এমন একটি নোডে পৌঁছাবেন যেখানে নিচের দিকের অন্তত কিছু অংশের জন্য কোনো ভিন্ন পথ নেই। পথ বরাবর 15টি পর্যন্ত স্পার্স NULL নোড তৈরি করা এড়াতে, আমরা [ encodedPath, key ] ফর্মের একটি extension নোড সেট আপ করে নিচের দিকে নামার শর্টকাট করি, যেখানে encodedPath-এ এগিয়ে যাওয়ার জন্য "আংশিক পথ" থাকে (নিচে বর্ণিত একটি কমপ্যাক্ট এনকোডিং ব্যবহার করে), এবং key হলো পরবর্তী DB লুকআপের জন্য।

একটি leaf নোডের জন্য, যা encodedPath-এর প্রথম নিবলে একটি ফ্ল্যাগ দ্বারা চিহ্নিত করা যেতে পারে, পথটি পূর্ববর্তী সমস্ত নোডের পথের অংশগুলোকে এনকোড করে এবং আমরা সরাসরি value খুঁজতে পারি।

তবে, উপরের এই অপ্টিমাইজেশনটি অস্পষ্টতা তৈরি করে।

নিবলে পথ অতিক্রম করার সময়, আমাদের অতিক্রম করার জন্য বিজোড় সংখ্যক নিবল থাকতে পারে, কিন্তু যেহেতু সমস্ত ডেটা bytes ফর্ম্যাটে সংরক্ষিত থাকে। উদাহরণস্বরূপ, নিবল 1 এবং নিবল 01-এর মধ্যে পার্থক্য করা সম্ভব নয় (উভয়কেই <01> হিসেবে সংরক্ষণ করতে হবে)। বিজোড় দৈর্ঘ্য নির্দিষ্ট করতে, আংশিক পথের আগে একটি ফ্ল্যাগ যুক্ত করা হয়।

স্পেসিফিকেশন: ঐচ্ছিক টার্মিনেটরসহ হেক্স সিকোয়েন্সের কমপ্যাক্ট এনকোডিং

উপরে বর্ণিত বিজোড় বনাম জোড় অবশিষ্ট আংশিক পথের দৈর্ঘ্য এবং লিফ বনাম এক্সটেনশন নোড উভয়ের ফ্ল্যাগিং যেকোনো 2-আইটেম নোডের আংশিক পথের প্রথম নিবলে থাকে। এর ফলে নিচের বিষয়গুলো ঘটে:

হেক্স ক্যারেক্টারবিটনোড টাইপ আংশিকপথের দৈর্ঘ্য
00000এক্সটেনশনজোড়
10001এক্সটেনশনবিজোড়
20010টার্মিনেটিং (লিফ)জোড়
30011টার্মিনেটিং (লিফ)বিজোড়

জোড় অবশিষ্ট পথের দৈর্ঘ্যের জন্য (0 বা 2), সর্বদা আরেকটি 0 "প্যাডিং" নিবল অনুসরণ করবে।

উদাহরণ:

    > [1, 2, 3, 4, 5, ...]
    '11 23 45'
    > [0, 1, 2, 3, 4, 5, ...]
    '00 01 23 45'
    > [0, f, 1, c, b, 8, 10]
    '20 0f 1c b8'
    > [f, 1, c, b, 8, 10]
    '3f 1c b8'

মার্কেল প্যাট্রিসিয়া ট্রাই-তে একটি নোড পাওয়ার জন্য বর্ধিত কোড নিচে দেওয়া হলো:

উদাহরণ ট্রাই

ধরুন আমরা এমন একটি ট্রাই চাই যাতে চারটি পথ/মান পেয়ার রয়েছে ('do', 'verb'), ('dog', 'puppy'), ('doge', 'coins'), ('horse', 'stallion')

প্রথমে, আমরা পথ এবং মান উভয়কেই bytes-এ রূপান্তর করি। নিচে, পথগুলোর প্রকৃত বাইট উপস্থাপনাগুলো <> দ্বারা চিহ্নিত করা হয়েছে, যদিও সহজে বোঝার জন্য মানগুলো এখনও স্ট্রিং হিসেবে দেখানো হয়েছে, যা '' দ্বারা চিহ্নিত করা হয়েছে (সেগুলোও আসলে bytes হবে):

<64 6f> : 'verb'
    <64 6f 67> : 'puppy'
    <64 6f 67 65> : 'coins'
    <68 6f 72 73 65> : 'stallion'

এখন, আমরা অন্তর্নিহিত DB-তে নিচের কী/ভ্যালু পেয়ারগুলো দিয়ে এমন একটি ট্রাই তৈরি করি:

rootHash: [ <16>, hashA ]
    hashA:    [ <>, <>, <>, <>, hashB, <>, <>, <>, [ <20 6f 72 73 65>, 'stallion' ], <>, <>, <>, <>, <>, <>, <>, <> ]
    hashB:    [ <00 6f>, hashC ]
    hashC:    [ <>, <>, <>, <>, <>, <>, hashD, <>, <>, <>, <>, <>, <>, <>, <>, <>, 'verb' ]
    hashD:    [ <17>, [ <>, <>, <>, <>, <>, <>, [ <35>, 'coins' ], <>, <>, <>, <>, <>, <>, <>, <>, <>, 'puppy' ] ]

যখন একটি নোড অন্য নোডের ভেতরে রেফারেন্স করা হয়, তখন যা অন্তর্ভুক্ত করা হয় তা হলো keccak256(rlp.encode(node)), যদি len(rlp.encode(node)) >= 32 হয় অন্যথায় node যেখানে rlp.encode হলো RLP এনকোডিং ফাংশন।

মনে রাখবেন যে একটি ট্রাই আপডেট করার সময়, একজনকে একটি স্থায়ী লুকআপ টেবিলে কী/ভ্যালু পেয়ার (keccak256(x), x) সংরক্ষণ করতে হবে যদি নতুন তৈরি করা নোডের দৈর্ঘ্য >= 32 হয়। তবে, যদি নোডটি এর চেয়ে ছোট হয়, তবে কিছু সংরক্ষণ করার প্রয়োজন নেই, কারণ ফাংশন f(x) = x রিভার্সিবল।

ইথেরিয়ামে ট্রাই

ইথেরিয়ামের এক্সিকিউশন লেয়ারের সমস্ত মার্কেল ট্রাই একটি মার্কেল প্যাট্রিসিয়া ট্রাই ব্যবহার করে।

একটি ব্লক হেডার থেকে এই ট্রাইগুলোর 3টির 3টি রুট রয়েছে।

  1. stateRoot
  2. transactionsRoot
  3. receiptsRoot

স্টেট ট্রাই

একটি গ্লোবাল স্টেট ট্রাই রয়েছে এবং প্রতিবার ক্লায়েন্ট একটি ব্লক প্রসেস করার সময় এটি আপডেট করা হয়। এতে, একটি path সর্বদা: keccak256(ethereumAddress) এবং একটি value সর্বদা: rlp(ethereumAccount)। আরও নির্দিষ্টভাবে একটি ইথেরিয়াম account হলো [nonce,balance,storageRoot,codeHash]-এর একটি 4 আইটেমের অ্যারে। এই পর্যায়ে, এটি লক্ষণীয় যে এই storageRoot হলো আরেকটি প্যাট্রিসিয়া ট্রাই-এর রুট:

স্টোরেজ ট্রাই

স্টোরেজ ট্রাই হলো যেখানে সমস্ত কন্ট্রাক্ট ডেটা থাকে। প্রতিটি অ্যাকাউন্টের জন্য একটি আলাদা স্টোরেজ ট্রাই রয়েছে। একটি নির্দিষ্ট ঠিকানায় নির্দিষ্ট স্টোরেজ পজিশনে মানগুলো পুনরুদ্ধার করতে স্টোরেজ ঠিকানা, স্টোরেজে সংরক্ষিত ডেটার পূর্ণসংখ্যা পজিশন এবং ব্লক আইডি প্রয়োজন। এগুলোকে এরপর জেসন-আরপিসি API-তে সংজ্ঞায়িত eth_getStorageAt-এ আর্গুমেন্ট হিসেবে পাস করা যেতে পারে, যেমন, ঠিকানা 0x295a70b2de5e3953354a6a8344e616ed314d7251-এর জন্য স্টোরেজ স্লট 0-তে ডেটা পুনরুদ্ধার করতে:

curl -X POST --data '{"jsonrpc":"2.0", "method": "eth_getStorageAt", "params": ["0x295a70b2de5e3953354a6a8344e616ed314d7251", "0x0", "latest"], "id": 1}' localhost:8545

{"jsonrpc":"2.0","id":1,"result":"0x00000000000000000000000000000000000000000000000000000000000004d2"}

স্টোরেজে অন্যান্য উপাদান পুনরুদ্ধার করা কিছুটা বেশি জটিল কারণ স্টোরেজ ট্রাই-তে পজিশনটি প্রথমে গণনা করতে হবে। পজিশনটি ঠিকানা এবং স্টোরেজ পজিশনের keccak256 হ্যাশ হিসেবে গণনা করা হয়, উভয়ই 32 বাইট দৈর্ঘ্য পর্যন্ত শূন্য দিয়ে লেফট-প্যাড করা থাকে। উদাহরণস্বরূপ, ঠিকানা 0x391694e7e0b0cce554cb130d723a9d27458f9298-এর জন্য স্টোরেজ স্লট 1-এ ডেটার পজিশন হলো:

keccak256(decodeHex("000000000000000000000000391694e7e0b0cce554cb130d723a9d27458f9298" + "0000000000000000000000000000000000000000000000000000000000000001"))

একটি গো ইথেরিয়াম (geth) কনসোলে, এটি নিচের মতো করে গণনা করা যেতে পারে:

> var key = "000000000000000000000000391694e7e0b0cce554cb130d723a9d27458f9298" + "0000000000000000000000000000000000000000000000000000000000000001"
undefined
> web3.sha3(key, {"encoding": "hex"})
"0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9"

তাই path হলো keccak256(<6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9>)। এটি এখন আগের মতো স্টোরেজ ট্রাই থেকে ডেটা পুনরুদ্ধার করতে ব্যবহার করা যেতে পারে:

curl -X POST --data '{"jsonrpc":"2.0", "method": "eth_getStorageAt", "params": ["0x295a70b2de5e3953354a6a8344e616ed314d7251", "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9", "latest"], "id": 1}' localhost:8545

{"jsonrpc":"2.0","id":1,"result":"0x000000000000000000000000000000000000000000000000000000000000162e"}

দ্রষ্টব্য: একটি ইথেরিয়াম অ্যাকাউন্টের জন্য storageRoot ডিফল্টভাবে ফাঁকা থাকে যদি এটি কোনো চুক্তি অ্যাকাউন্ট না হয়।

ট্রানজ্যাকশন ট্রাই

প্রতিটি ব্লকের জন্য একটি আলাদা ট্রানজ্যাকশন ট্রাই রয়েছে, যা আবার (key, value) পেয়ার সংরক্ষণ করে। এখানে একটি পথ হলো: rlp(transactionIndex) যা এমন একটি কী-কে উপস্থাপন করে যা নিচের দ্বারা নির্ধারিত একটি মানের সাথে মিলে যায়:

if legacyTx:
  value = rlp(tx)
else:
  value = TxType | encode(tx)

এই সম্পর্কে আরও তথ্য EIP-2718 (opens in a new tab) ডকুমেন্টেশনে পাওয়া যাবে।

রসিদ ট্রাই

প্রতিটি ব্লকের নিজস্ব রসিদ ট্রাই রয়েছে। এখানে একটি path হলো: rlp(transactionIndex)transactionIndex হলো যে ব্লকে এটি অন্তর্ভুক্ত করা হয়েছিল তার ভেতরের সূচক। রসিদ ট্রাই কখনো আপডেট করা হয় না। ট্রানজ্যাকশন ট্রাই-এর মতো, বর্তমান এবং লিগ্যাসি রসিদ রয়েছে। রসিদ ট্রাই-তে একটি নির্দিষ্ট রসিদ কোয়েরি করতে, এর ব্লকে ট্রানজ্যাকশনের সূচক, রসিদ পেলোড এবং ট্রানজ্যাকশনের ধরন প্রয়োজন। রিটার্ন করা রসিদটি Receipt ধরনের হতে পারে যা TransactionType এবং ReceiptPayload-এর কনক্যাটেনেশন হিসেবে সংজ্ঞায়িত, অথবা এটি LegacyReceipt ধরনের হতে পারে যা rlp([status, cumulativeGasUsed, logsBloom, logs]) হিসেবে সংজ্ঞায়িত।

এই সম্পর্কে আরও তথ্য EIP-2718 (opens in a new tab) ডকুমেন্টেশনে পাওয়া যাবে।

আরও পড়ুন