সিম্পল সিরিয়ালাইজ
সিম্পল সিরিয়ালাইজ (SSZ) হলো বিকন চেইনে ব্যবহৃত সিরিয়ালাইজেশন পদ্ধতি। এটি পিয়ার ডিসকভারি প্রোটোকল ব্যতীত কনসেনসাস লেয়ারের সর্বত্র এক্সিকিউশন লেয়ারে ব্যবহৃত RLP সিরিয়ালাইজেশনকে প্রতিস্থাপন করে। RLP সিরিয়ালাইজেশন সম্পর্কে আরও জানতে, রিকার্সিভ-লেংথ প্রিফিক্স (RLP) দেখুন। SSZ-কে ডিটারমিনিস্টিক এবং কার্যকরভাবে মার্কেল ট্রি তৈরি (Merkleize) করার জন্য ডিজাইন করা হয়েছে। SSZ-এর দুটি উপাদান রয়েছে বলে ধরে নেওয়া যেতে পারে: একটি সিরিয়ালাইজেশন স্কিম এবং একটি মার্কেল ট্রি তৈরির স্কিম যা সিরিয়ালাইজ করা ডেটা স্ট্রাকচারের সাথে কার্যকরভাবে কাজ করার জন্য ডিজাইন করা হয়েছে।
SSZ কীভাবে কাজ করে?
সিরিয়ালাইজেশন
SSZ হলো এমন একটি সিরিয়ালাইজেশন স্কিম যা সেলফ-ডেসক্রাইবিং (স্ব-বর্ণনাকারী) নয় - বরং এটি এমন একটি স্কিমার উপর নির্ভর করে যা আগে থেকেই জানা থাকতে হবে। SSZ সিরিয়ালাইজেশনের লক্ষ্য হলো যেকোনো জটিলতার অবজেক্টকে বাইটের স্ট্রিং হিসেবে উপস্থাপন করা। "বেসিক টাইপ" বা সাধারণ ধরনের জন্য এটি একটি খুব সহজ প্রক্রিয়া। উপাদানটিকে কেবল হেক্সাডেসিমাল বাইটে রূপান্তর করা হয়। বেসিক টাইপগুলোর মধ্যে রয়েছে:
- আনসাইনড ইন্টিজার (unsigned integers)
- বুলিয়ান (Booleans)
জটিল "কম্পোজিট" টাইপের ক্ষেত্রে, সিরিয়ালাইজেশন আরও জটিল কারণ কম্পোজিট টাইপে একাধিক উপাদান থাকে যেগুলোর ধরন বা আকার ভিন্ন হতে পারে, অথবা উভয়ই ভিন্ন হতে পারে। যেখানে এই অবজেক্টগুলোর সবগুলোর নির্দিষ্ট দৈর্ঘ্য থাকে (অর্থাৎ, উপাদানগুলোর আকার তাদের প্রকৃত মান নির্বিশেষে সর্বদা স্থির থাকবে), সেখানে সিরিয়ালাইজেশন হলো কম্পোজিট টাইপের প্রতিটি উপাদানকে ক্রমানুসারে লিটল-এন্ডিয়ান (little-endian) বাইটস্ট্রিংয়ে রূপান্তর করা। এই বাইটস্ট্রিংগুলো একসাথে যুক্ত করা হয়। সিরিয়ালাইজ করা অবজেক্টে নির্দিষ্ট দৈর্ঘ্যের উপাদানগুলোর বাইটলিস্ট উপস্থাপনা ঠিক সেই ক্রমেই থাকে, যেভাবে সেগুলো ডিসিরিয়ালাইজ করা অবজেক্টে উপস্থিত থাকে।
পরিবর্তনশীল দৈর্ঘ্যের (variable lengths) টাইপগুলোর ক্ষেত্রে, সিরিয়ালাইজ করা অবজেক্টে সেই উপাদানের অবস্থানে প্রকৃত ডেটা একটি "অফসেট" (offset) মান দ্বারা প্রতিস্থাপিত হয়। প্রকৃত ডেটা সিরিয়ালাইজ করা অবজেক্টের শেষে একটি হিপে (heap) যুক্ত হয়। অফসেট মানটি হলো হিপে প্রকৃত ডেটা শুরু হওয়ার সূচক, যা প্রাসঙ্গিক বাইটগুলোর পয়েন্টার হিসেবে কাজ করে।
নিচের উদাহরণটি দেখায় যে কীভাবে নির্দিষ্ট এবং পরিবর্তনশীল-দৈর্ঘ্যের উভয় উপাদানযুক্ত একটি কন্টেইনারের জন্য অফসেটিং কাজ করে:
struct Dummy {
number1: u64,
number2: u64,
vector: Vec<u8>,
number3: u64
}
dummy = Dummy{
number1: 37,
number2: 55,
vector: vec![1,2,3,4],
number3: 22,
}
serialized = ssz.serialize(dummy)
serialized-এর নিচের মতো স্ট্রাকচার থাকবে (এখানে কেবল 4 বিটে প্যাড করা হয়েছে, বাস্তবে 32 বিটে প্যাড করা হয় এবং স্পষ্টতার জন্য int উপস্থাপনা রাখা হয়েছে):
[37, 0, 0, 0, 55, 0, 0, 0, 16, 0, 0, 0, 22, 0, 0, 0, 1, 2, 3, 4]
------------ ----------- ----------- ----------- ----------
| | | | |
number1 number2 vector-এর number 3 vector-এর
জন্য offset জন্য value
বোঝার সুবিধার্থে লাইনগুলোতে ভাগ করা হয়েছে:
[
37, 0, 0, 0, # `number1`-এর লিটল-এন্ডিয়ান এনকোডিং।
55, 0, 0, 0, # `number2`-এর লিটল-এন্ডিয়ান এনকোডিং।
16, 0, 0, 0, # "অফসেট" যা নির্দেশ করে যে `vector`-এর মান কোথায় শুরু হয় (লিটল-এন্ডিয়ান 16)।
22, 0, 0, 0, # `number3`-এর লিটল-এন্ডিয়ান এনকোডিং।
1, 2, 3, 4, # `vector`-এর প্রকৃত মানগুলো।
]
এটি এখনও একটি সরলীকরণ - উপরের স্কিম্যাটিক্সের ইন্টিজার এবং শূন্যগুলো আসলে বাইটলিস্ট হিসেবে সংরক্ষিত হবে, ঠিক এরকম:
[
10100101000000000000000000000000 # `number1`-এর লিটল-এন্ডিয়ান এনকোডিং
10110111000000000000000000000000 # `number2`-এর লিটল-এন্ডিয়ান এনকোডিং।
10010000000000000000000000000000 # "অফসেট" যা নির্দেশ করে যে `vector`-এর মান কোথায় শুরু হয় (লিটল-এন্ডিয়ান 16)।
10010110000000000000000000000000 # `number3`-এর লিটল-এন্ডিয়ান এনকোডিং।
10000001100000101000001110000100 # `bytes` ফিল্ডের প্রকৃত মান।
]
সুতরাং পরিবর্তনশীল-দৈর্ঘ্যের টাইপগুলোর প্রকৃত মানগুলো সিরিয়ালাইজ করা অবজেক্টের শেষে একটি হিপে সংরক্ষিত থাকে এবং তাদের অফসেটগুলো ফিল্ডের ক্রমানুসারে সঠিক অবস্থানে সংরক্ষিত থাকে।
এছাড়াও কিছু বিশেষ ক্ষেত্র রয়েছে যেগুলোর জন্য নির্দিষ্ট প্রক্রিয়ার প্রয়োজন হয়, যেমন BitList টাইপ, যার জন্য সিরিয়ালাইজেশনের সময় একটি লেংথ ক্যাপ (length cap) যোগ করতে হয় এবং ডিসিরিয়ালাইজেশনের সময় তা সরিয়ে ফেলতে হয়। সম্পূর্ণ বিবরণ SSZ স্পেক (opens in a new tab)-এ পাওয়া যাবে।
এই অবজেক্টটিকে ডিসিরিয়ালাইজ করার জন্য স্কিমা প্রয়োজন। স্কিমা সিরিয়ালাইজ করা ডেটার সুনির্দিষ্ট লেআউট সংজ্ঞায়িত করে যাতে প্রতিটি নির্দিষ্ট উপাদানকে বাইটের একটি ব্লব থেকে সঠিক ধরন, মান, আকার এবং অবস্থান সহ কোনো অর্থপূর্ণ অবজেক্টে ডিসিরিয়ালাইজ করা যায়। স্কিমাই ডিসিরিয়ালাইজারকে বলে দেয় কোন মানগুলো প্রকৃত মান এবং কোনগুলো অফসেট। কোনো অবজেক্ট সিরিয়ালাইজ করা হলে সমস্ত ফিল্ডের নাম মুছে যায়, কিন্তু স্কিমা অনুযায়ী ডিসিরিয়ালাইজেশনের সময় সেগুলো পুনরায় তৈরি হয়।
মার্কেল ট্রি তৈরি (Merkleization)
এই SSZ সিরিয়ালাইজ করা অবজেক্টটিকে এরপর মার্কেল ট্রি-তে রূপান্তর (merkleized) করা যেতে পারে - অর্থাৎ একই ডেটার একটি মার্কেল ট্রি উপস্থাপনায় রূপান্তরিত করা যায়। প্রথমে, সিরিয়ালাইজ করা অবজেক্টে 32-বাইট চাঙ্কের (chunks) সংখ্যা নির্ধারণ করা হয়। এগুলো হলো ট্রির "লিভস" (leaves) বা পাতা। লিভসের মোট সংখ্যা অবশ্যই 2-এর পাওয়ার হতে হবে যাতে লিভসগুলোকে একসাথে হ্যাশিং করলে শেষ পর্যন্ত একটি একক হ্যাশ-ট্রি-রুট (hash-tree-root) তৈরি হয়। যদি স্বাভাবিকভাবে এমনটি না হয়, তবে 32 বাইট শূন্য (zeros) ধারণকারী অতিরিক্ত লিভস যোগ করা হয়। চিত্রের মাধ্যমে:
hash tree root
/ \
/ \
/ \
/ \
leaves 1 ও 2 leaves 3 ও 4
এর hash এর hash
/ \ / \
/ \ / \
/ \ / \
leaf1 leaf2 leaf3 leaf4
এমন কিছু ক্ষেত্রও রয়েছে যেখানে ট্রির লিভসগুলো উপরের উদাহরণের মতো স্বাভাবিকভাবে সমানভাবে বণ্টিত হয় না। উদাহরণস্বরূপ, leaf 4 এমন একটি কন্টেইনার হতে পারে যেখানে একাধিক উপাদান রয়েছে, যার জন্য মার্কেল ট্রিতে অতিরিক্ত "গভীরতা" (depth) যোগ করার প্রয়োজন হয়, যা একটি অসম ট্রি তৈরি করে।
এই ট্রি উপাদানগুলোকে leaf X, node X ইত্যাদি হিসেবে উল্লেখ করার পরিবর্তে, আমরা সেগুলোকে জেনারেলাইজড সূচক (generalized indices) দিতে পারি, যা root = 1 দিয়ে শুরু হয় এবং প্রতিটি লেভেল বরাবর বাম থেকে ডানে গণনা করা হয়। এটিই হলো উপরে ব্যাখ্যা করা জেনারেলাইজড সূচক। সিরিয়ালাইজ করা তালিকার প্রতিটি উপাদানের একটি জেনারেলাইজড সূচক থাকে যা 2**depth + idx-এর সমান, যেখানে idx হলো সিরিয়ালাইজ করা অবজেক্টে এর জিরো-ইনডেক্সড (zero-indexed) অবস্থান এবং depth হলো মার্কেল ট্রির লেভেলের সংখ্যা, যা উপাদানগুলোর (লিভস) সংখ্যার বেস-টু লগারিদম (base-two logarithm) হিসেবে নির্ধারণ করা যেতে পারে।
জেনারেলাইজড সূচক
জেনারেলাইজড সূচক হলো একটি ইন্টিজার যা বাইনারি মার্কেল ট্রিতে একটি নোডকে উপস্থাপন করে, যেখানে প্রতিটি নোডের একটি জেনারেলাইজড সূচক 2 ** depth + index in row থাকে।
1 --depth = 0 2**0 + 0 = 1
2 3 --depth = 1 2**1 + 0 = 2, 2**1+1 = 3
4 5 6 7 --depth = 2 2**2 + 0 = 4, 2**2 + 1 = 5...
এই উপস্থাপনাটি মার্কেল ট্রির প্রতিটি ডেটার জন্য একটি নোড সূচক প্রদান করে।
মাল্টিপ্রুফ (Multiproofs)
কোনো নির্দিষ্ট উপাদানকে উপস্থাপনকারী জেনারেলাইজড সূচকগুলোর তালিকা প্রদান করলে তা আমাদের হ্যাশ-ট্রি-রুটের বিপরীতে যাচাই করার সুযোগ দেয়। এই রুটটি হলো আমাদের কাছে বাস্তবতার স্বীকৃত সংস্করণ। আমাদের দেওয়া যেকোনো ডেটাকে মার্কেল ট্রির সঠিক স্থানে (এর জেনারেলাইজড সূচক দ্বারা নির্ধারিত) সন্নিবেশ করে এবং রুটটি অপরিবর্তিত থাকে কিনা তা পর্যবেক্ষণ করে সেই বাস্তবতার বিপরীতে যাচাই করা যেতে পারে। স্পেকের এখানে (opens in a new tab) কিছু ফাংশন রয়েছে যা দেখায় যে কীভাবে জেনারেলাইজড সূচকগুলোর একটি নির্দিষ্ট সেটের বিষয়বস্তু যাচাই করার জন্য প্রয়োজনীয় নোডগুলোর ন্যূনতম সেট গণনা করতে হয়।
উদাহরণস্বরূপ, নিচের ট্রিতে সূচক 9-এর ডেটা যাচাই করার জন্য, আমাদের 8, 9, 5, 3, 1 সূচকগুলোর ডেটার হ্যাশ প্রয়োজন। (8,9)-এর হ্যাশ (4)-এর হ্যাশের সমান হওয়া উচিত, যা 5-এর সাথে হ্যাশ করে 2 তৈরি করে, এবং এটি 3-এর সাথে হ্যাশ করে ট্রি রুট 1 তৈরি করে। যদি 9-এর জন্য ভুল ডেটা দেওয়া হয়, তবে রুটটি পরিবর্তিত হবে - আমরা এটি শনাক্ত করতে পারব এবং ব্রাঞ্চটি যাচাই করতে ব্যর্থ হব।
* = প্রুফ তৈরি করার জন্য প্রয়োজনীয় ডেটা
1*
2 3*
4 5* 6 7
8* 9* 10 11 12 13 14 15