पिछले ट्यूटोरियल में, हमने सीखा कि एक program में पास किए गए accounts को कैसे पढ़ा जाता है। हमने देखा कि account.try_borrow_data() को कॉल करने पर account के data field का एक रेफरेंस raw byte slice के रूप में मिलता है, उदाहरण के लिए [0x01, 0x00, 0x00, 0x00]।
Solana सभी account data को bytes के रूप में स्टोर करता है। Rust structs जैसे higher-level data structures के साथ काम करने के लिए, हम structs को on-chain स्टोर करने के लिए bytes में बदलने के लिए serialization का उपयोग करते हैं, और उन्हें पढ़ते समय उन bytes को वापस structs में बदलने के लिए deserialization का उपयोग करते हैं। Solana अपने स्टैण्डर्ड serialization फॉर्मेट के रूप में Borsh का उपयोग करता है।
यह लेख बताता है कि Borsh serialization कैसे काम करता है और इन raw bytes को कैसे इंटरप्रेट किया जाए।
इस ट्यूटोरियल में, हम निम्नलिखित चीजें दिखाएंगे:
- Serialization क्या है और Solana में Borsh serialization कैसे काम करता है
- Serialized account data को कैसे पढ़ें और इंटरप्रेट करें
- जब आप बिना data वाले किसी account को पढ़ने की कोशिश करते हैं तो वह कैसा दिखता है
Serialization और Deserialization क्या है?
Serialization वह प्रक्रिया है जिसमें data structures (जैसे कि Rust struct या string) को bytes के एक क्रम (sequence) में बदला जाता है ताकि उन्हें स्टोर या ट्रांसमिट किया जा सके। Deserialization इसकी विपरीत प्रक्रिया है - उन bytes को वापस मूल data structure में बदलना।
उदाहरण के लिए, यदि आपके पास एक struct है जिसमें counter की वैल्यू 42 है (जिसे u64 के रूप में स्टोर किया गया है), तो serialization इसे 8 bytes में बदल देता है: [0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]। पहला byte 0x2A little-endian क्रम में वैल्यू का least significant byte रखता है, और शेष 7 bytes शून्य (zeros) होते हैं क्योंकि u64 मेमोरी में 8 bytes के रूप में स्टोर होता है। बाद में, जब आपको उस data को पढ़ने की आवश्यकता होती है, तो deserialization उन bytes को वापस आपके struct में बदल देता है।
Borsh Serialization क्या है?
Borsh (Binary Object Representation Serializer for Hashing) एक serialization फॉर्मेट है जो Rust structs को bytes में और वापस structs में बदलने के नियम परिभाषित करता है। Solana अपने स्टैण्डर्ड serialization फॉर्मेट के रूप में Borsh का उपयोग करता है क्योंकि यह:
- Deterministic: समान data हमेशा समान bytes बनाता है (और समान bytes हमेशा समान data बनाते हैं)
- Compact: यह fields के बीच बिना किसी अतिरिक्त मेटाडेटा या पैडिंग के प्रभावी ढंग से data स्टोर करता है (fixed-size types अपने स्टैण्डर्ड byte साइज का उपयोग करते हैं, variable-length types केवल अपनी आवश्यकतानुसार साइज और साथ में 4-byte length prefix का उपयोग करते हैं)
borsh Rust crate उस serialization फॉर्मेट को इम्प्लीमेंट करता है जिसका उपयोग Solana programs करते हैं।
Solana programs instruction data और on-chain account data दोनों को serialize करने के लिए Borsh का उपयोग करते हैं। Accounts के लिए, Borsh Rust structs और उनके fields (strings, integers, booleans, vectors, आदि) को raw bytes में बदल देता है जो account’s data field में स्टोर होते हैं।
Borsh Serialization कैसे काम करता है
Native Rust programs में, हम उस account data को रीप्रेजेंट करने के लिए structs डिफाइन करते हैं जिसे हम स्टोर करना चाहते हैं (उसी तरह जैसे आप Anchor में account structs डिफाइन करते हैं)। Borsh फिर इन structs को bytes में serialize करता है जो account के data field में भरे जाते हैं। Serialize होने पर, struct fields उसी क्रम में account के data field में एक साथ (contiguously) रखे जाते हैं जिस क्रम में उन्हें डिक्लेयर किया गया था।
इसका बेसिक फ्लो नीचे दिए गए डायग्राम में दिखाया गया है:

यह डायग्राम दिखाता है:
- Serialization: आपका Rust struct (उदा.,
CounterData { count: 42 }) Borsh का उपयोग करके raw bytes में बदल दिया जाता है - Storage: उन bytes को on-chain account के data field में स्टोर किया जाता है
- Deserialization: Account को पढ़ते समय, Borsh उन bytes को वापस आपके struct में बदल देता है
Borsh Serialization के बाद Solana Account Data कैसा दिखता है?
Anchor में, serialization को ऐब्सट्रैक्ट कर दिया गया है, लेकिन native Rust programs में हम अपने structs में #[derive(BorshSerialize, BorshDeserialize)] एट्रिब्यूट जोड़ते हैं। यह Borsh लाइब्रेरी को compile time पर स्वचालित रूप से serialization और deserialization कोड जनरेट करने के लिए कहता है।
उदाहरण के लिए, यहाँ एक CounterData struct है जो एक counter वैल्यू को u64 के रूप में स्टोर करता है:
#[derive(BorshSerialize, BorshDeserialize)]
pub struct CounterData {
pub count: u64,
}
यदि count की वैल्यू 42 है, तो BorshSerialize एट्रिब्यूट इस struct को इसमें serialize करेगा:

यहाँ यह होता है:
- Borsh हमारे count की वैल्यू (42) लेता है और इसे हेक्साडेसिमल में बदल देता है: डेसिमल में 42 = हेक्स में
0x2A - चूँकि count को
u64के रूप में डिफाइन किया गया है, इसलिए Borsh इसे दर्शाने के लिए little-endian फॉर्मेट में 8 bytes का उपयोग करता है 0x2Aके बाद बचे हुए 7 bytes शून्य (zeros) होते हैं क्योंकि एकu648 bytes की जगह लेता है
Borsh Variable-Length Data को कैसे Serialize करता है
इससे पहले कि हम अधिक जटिल उदाहरण देखें, आइए समझें कि Borsh उन data types को कैसे हैंडल करता है जिनकी लम्बाई (length) फिक्स नहीं होती है, जैसे strings और vectors।
u64, bool, और u8 जैसे fixed data types (जो अपनी वैल्यू की परवाह किए बिना समान संख्या में bytes का उपयोग करते हैं) के विपरीत, String और Vec<T> जैसे variable-length data types का साइज उनके कंटेंट के आधार पर अलग-अलग होता है।
Variable-length types के लिए, Borsh length prefixing का उपयोग करता है: यह पहले data का साइज लिखता है, फिर वास्तविक data। यह Borsh को बताता है कि deserializing करते समय कितने bytes पढ़ने हैं (इसके बिना, Borsh को पता नहीं चलेगा कि एक field कहाँ खत्म होता है और दूसरा कहाँ शुरू होता है)।
यह इस प्रकार काम करता है:
- सबसे पहले, Borsh little-endian फॉर्मेट में data की length को
u32(4 bytes) के रूप में serialize करता है - फिर, यह वास्तविक data bytes को serialize करता है
उदाहरण के लिए, यदि हमारे पास एक string “hi” है:
- सबसे पहले, Borsh little-endian में length को
u32के रूप में serialize करता है:[0x02, 0x00, 0x00, 0x00](2 bytes के लिए)। चूँकि length कोu32के रूप में स्टोर किया जाता है, इसलिए string का अधिकतम साइज सैद्धांतिक रूप से2^32 - 1bytes हो सकता है - अंत में, Borsh “hi” के लिए वास्तविक UTF-8 bytes को serialize करता है:
[0x68, 0x69]
इससे हमें अंतिम परिणाम मिलता है:

यह length prefix deserialization के दौरान Borsh को यह जानने में मदद करता है कि variable-length type के लिए ठीक कितने bytes पढ़ने हैं।
Borsh Multiple Fields वाले Solana Account Struct को कैसे Serialize करता है?
मान लीजिए कि हमारे पास एक storage account के लिए user की जानकारी दर्शाने हेतु कई fields वाला एक UserData struct है:
#[derive(BorshSerialize, BorshDeserialize)]
struct UserData {
active: bool, // 1 byte: 0x01 for true, 0x00 for false
age: u8, // 1 byte
name: String, // 4 bytes (length) + UTF-8 bytes
scores: Vec<u8>, // 4 bytes (length) + individual u8 values
}
let user = UserData {
active: true,
age: 25,
name: "hi".to_string(),
scores: vec![95, 87, 92],
};
Borsh struct fields को सख्ती से उसी क्रम में serialize करता है जिस क्रम में उन्हें struct में डिफाइन किया गया है, चाहे वे fixed-size हों या variable-length (dynamically-sized)। Fixed-size और variable-length fields की प्लेसमेंट इस बात को प्रभावित नहीं करती कि Borsh उन्हें कैसे हैंडल करता है - प्रत्येक field को उसके type के नियमों के अनुसार serialize किया जाता है (fixed-size types अपने स्टैण्डर्ड byte साइज का उपयोग करते हैं, variable-length types length-prefixing का उपयोग करते हैं), और सभी fields को डिक्लेरेशन के क्रम में क्रमिक रूप से (sequentially) रखा जाता है। यहाँ यह होता है:
- सबसे पहले, Borsh
activefield (true) को 1 byte में serialize करता है:0x01। - फिर यह
age(25) को 1 byte में serialize करता है:0x19। - इसके बाद, यह
namefield (“hi”) को serialize करता है। चूँकि strings dynamically-sized होते हैं, Borsh length-prefixed एप्रोच का उपयोग करता है:- यह पहले length को little-endian में
u32के रूप में लिखता है:[0x02, 0x00, 0x00, 0x00](2 bytes) - फिर “hi” के लिए वास्तविक UTF-8 bytes (
[0x68, 0x69]) लिखता है
- यह पहले length को little-endian में
- अंत में, यह समान length-prefixed एप्रोच का उपयोग करके
scoresvector[95, 87, 92]को serialize करता है:- यह little-endian में vector की length को
u32के रूप में लिखता है (3 items =[0x03, 0x00, 0x00, 0x00]) - फिर प्रत्येक
u8वैल्यू लिखता है:[0x5F, 0x57, 0x5C]।
- यह little-endian में vector की length को
इन सभी bytes को उसी क्रम में एक साथ जोड़ दिया जाता है जिस क्रम में fields डिक्लेयर किए गए हैं, जिससे हमें अंतिम परिणाम मिलता है:

यह दिखाता है कि कैसे Borsh String और Vec सहित multiple fields वाले account को हैंडल करता है।
हम Serialized Account Data को वापस कैसे पढ़ते हैं?
एक Solana account से अपना data वापस पाने के लिए, हम इसे deserialize करते हैं। Borsh लाइब्रेरी एक try_from_slice फ़ंक्शन प्रदान करती है जो bytes को उसी क्रम में पढ़कर deserialization को हैंडल करता है जिस क्रम में वे serialize किए गए थे और मूल struct को फिर से बनाता है (reconstruct करता है)।
इसलिए एक native program में पास किए गए Solana account के लिए, फ्लो इस प्रकार है:
- Account से raw bytes को पढ़ें
- उन bytes को मूल struct में deserialize करने के लिए Borsh crate से
try_from_sliceको कॉल करें
नीचे दिया गया कोड इसे व्यवहारिक रूप में दिखाता है। नीचे दिया गया read_user_account फ़ंक्शन एक कॉन्सेप्चुअल रीप्रेजेंटेशन है जो दर्शाता है कि deserialization के लिए try_from_slice का उपयोग कैसे करें। account पैरामीटर एक सामान्य Solana account को दर्शाता है जिसमें ऊपर दिए गए “Borsh Multiple Fields वाले Solana Account Struct को कैसे Serialize करता है?” सेक्शन से UserData struct (जिसमें active, age, name, और scores fields हैं) शामिल है।
use borsh::BorshDeserialize;
use solana_program::account_info::AccountInfo;
pub fn read_user_account(account: &AccountInfo) -> ProgramResult {
// First, we get the raw bytes from the account
let data = account.try_borrow_data()?;
// The raw bytes in the account's data field:
// [0x01, 0x19, 0x02, 0x00, 0x00, 0x00, 0x68, 0x69, 0x03, 0x00, 0x00, 0x00, 0x5F, 0x57, 0x5C]
// Then we use Borsh to deserialize these bytes back into our struct
let _user = UserData::try_from_slice(&data)?;
// We get back our original data:
// UserData { active: true, age: 25, name: "hi", scores: [95, 87, 92] }
Ok(())
}
अन्य Common Types के लिए Borsh Serialization के नियम
Solana accounts में इस्तेमाल होने वाले अन्य common field types जैसे booleans, numbers (u32, i32, u64), और Pubkeys के लिए, Borsh इन नियमों का पालन करता है:
| Type | Size | Format | Example |
|---|---|---|---|
bool |
1 byte | true के लिए 0x01, false के लिए 0x00 |
true → [0x01] |
u8 |
1 byte | Raw value | 42 → [0x2A] |
u16 |
2 bytes | Little-endian | 42 → [0x2A, 0x00] |
u32 |
4 bytes | Little-endian | 42 → [0x2A, 0x00, 0x00, 0x00] |
u64 |
8 bytes | Little-endian | 42 → [0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] |
u128 |
16 bytes | Little-endian | 16 bytes के साथ समान पैटर्न |
i8, i16, i32, i64, i128 |
Unsigned के समान | Little-endian, two’s complement | i8 के रूप में -1 → [0xFF], पॉजिटिव वैल्यूज unsigned की तरह ही serialize की जाती हैं |
Pubkey |
32 bytes | Raw bytes | Public key के 32 bytes |
हमने पहले जो अपेंड (append) करने का सिद्धांत देखा था, वह यहाँ भी लागू होता है। जब आपके पास इन types वाला एक struct होता है, तो Borsh क्रमानुसार प्रत्येक field से होकर गुजरता है और बिना किसी पैडिंग या अतिरिक्त मेटाडेटा के सभी bytes को एक साथ अपेंड कर देता है (यह serialized data को यथासंभव छोटा रखता है)।
Deserialization के बिना Raw Bytes पढ़ना
यदि आपको मेमोरी लेआउट पता है, तो आप deserializing के बिना मैन्युअल रूप से account data से विशिष्ट (specific) fields पढ़ सकते हैं। प्रोडक्शन कोड के लिए इसकी अनुशंसा नहीं की जाती है (यह त्रुटि-प्रवण/error-prone है), लेकिन इससे यह समझने में मदद मिलती है कि Borsh कैसे काम करता है।
मान लीजिए कि हमारे पास पहले वाला CounterData account है जिसमें केवल count: 42 field है, जिसे [0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] के रूप में serialize किया गया है। हम पहले 8 bytes निकालकर और उन्हें u64 में बदलकर मैन्युअल रूप से केवल count की वैल्यू पढ़ सकते हैं:
pub fn read_count_manually(account: &AccountInfo) -> ProgramResult {
let data = account.try_borrow_data()?;
// count is a u64, so it occupies the first 8 bytes
if data.len() >= 8 {
let count_bytes = &data[0..8];
let count = u64::from_le_bytes(count_bytes.try_into().unwrap());
msg!("Count value: {}", count); // This will print: Count value: 42
}
Ok(())
}
ध्यान दें: चूँकि CounterData में count एकमात्र field है, इसलिए इसकी 8-byte length से आगे पढ़ने का प्रयास करने पर panic होगा क्योंकि struct में कोई अतिरिक्त data नहीं है।
उपरोक्त कोड को रन करने पर 42 की count वैल्यू लॉग होगी।

यह काम करता है क्योंकि हम struct लेआउट को जानते हैं। count field पहले 8 bytes (byte पोज़िशन्स 0–7) लेता है, इसलिए हम उन्हें सीधे पढ़ सकते हैं और वापस u64 में बदल सकते हैं। एक वास्तविक (real) program में, हम इसके बजाय CounterData::try_from_slice(&data)? का उपयोग करेंगे, जो raw bytes से पूरे struct को स्वचालित रूप से deserialize करता है।
Account Metadata (Lamports, Owner, आदि) को एक्सेस करना
अभी तक हम account के data field के बारे में बात कर रहे थे, लेकिन जैसा कि हमने पिछले ट्यूटोरियल्स में सीखा है, Solana accounts में lamports, owner, pubkey, आदि जैसे अन्य महत्वपूर्ण fields भी होते हैं।
account.try_borrow_data()? हमें केवल data field देता है (जहाँ हमारे Borsh-serialized structs मौजूद होते हैं), लेकिन हमारे process_instruction फ़ंक्शन में पास किया गया AccountInfo struct हमें अन्य सभी account metadata का एक्सेस देता है जिन्हें Solana स्वचालित रूप से मेंटेन करता है।
हम नीचे दिए गए कोड में दिखाते हैं कि इन fields को कैसे एक्सेस किया जाए:
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
// Get the account's lamports (balance)
let lamports = account.lamports();
msg!("Account lamports: {}", lamports);
// Get the account's owner (program that controls it)
let owner = account.owner;
msg!("Account owner: {}", owner);
// Get the account's public key
let pubkey = account.key;
msg!("Account pubkey: {}", pubkey);
// Check if the account is a signer
let is_signer = account.is_signer;
msg!("Is signer: {}", is_signer);
// Check if the account is writable
let is_writable = account.is_writable;
msg!("Is writable: {}", is_writable);
Ok(())
}
जब हम उपरोक्त कोड रन करते हैं, तो हमें कुछ इस तरह मिलता है:

AccountInfo वह struct type है जो आपके program में एक Solana account को दर्शाता है। इसमें account का सभी मेटाडेटा (lamports, owner, आदि) होता है और यह data field तक पहुँचने के तरीके प्रदान करता है। जब हम account.try_borrow_data()? को कॉल करते हैं, तो हम केवल उस data field को एक्सेस कर रहे होते हैं जहाँ हमारे Borsh-serialized structs स्टोर होते हैं।
सारांश
इस ट्यूटोरियल में, हमने कवर किया कि Solana में Borsh serialization कैसे काम करता है:
- Serialization स्टोरेज के लिए Rust structs को bytes में बदलता है, जबकि deserialization bytes को वापस structs में बदल देता है
- Borsh Solana का स्टैण्डर्ड serialization फॉर्मेट है - यह deterministic और compact है
#[derive(BorshSerialize, BorshDeserialize)]एट्रिब्यूट आपके structs के लिए Borsh serialization को इनेबल करता है- Borsh Solana accounts के data field को serialize करता है, अन्य मेटाडेटा fields को नहीं
- Fixed-length types (जैसे
u64,bool) एक सुसंगत (consistent) संख्या में bytes का उपयोग करते हैं - Variable-length types (जैसे
String,Vec<T>) length prefixing का उपयोग करते हैं: length के लिए 4 bytes + वास्तविक data - Struct fields बिना किसी पैडिंग के क्रमानुसार उसी क्रम में serialize किए जाते हैं जिस क्रम में उन्हें डिक्लेयर किया गया है
AccountInfoसभी account metadata तक पहुँच (access) प्रदान करता है, जबकिaccount.try_borrow_data()?हमें केवल serialized data field देता है
अगले ट्यूटोरियल में, हम native Rust Solana programs में storage accounts बनाकर और उनके data को पढ़कर इस ज्ञान को व्यवहार में लाएंगे।
यह लेख Solana development पर एक ट्यूटोरियल सीरीज़ का हिस्सा है।