इस बिंदु तक, हमारे किसी भी ट्यूटोरियल ने “storage variables” का उपयोग नहीं किया है या कुछ भी स्थायी रूप से स्टोर नहीं किया है।
Solidity और Ethereum में, डेटा स्टोर करने का एक अधिक अनूठा (exotic) डिज़ाइन पैटर्न SSTORE2 या SSTORE3 है, जहाँ डेटा किसी अन्य स्मार्ट कॉन्ट्रैक्ट के बाइटकोड (bytecode) में स्टोर किया जाता है।
Solana में, यह कोई अनूठा डिज़ाइन पैटर्न नहीं है, यह सामान्य बात (norm) है!
याद रखें कि हम एक Solana प्रोग्राम के बाइटकोड को अपनी इच्छा से अपडेट कर सकते हैं (यदि हम मूल डिप्लॉयर हैं) जब तक कि प्रोग्राम को immutable (अपरिवर्तनीय) के रूप में मार्क न किया गया हो।
Solana डेटा स्टोरेज के लिए इसी तंत्र (mechanism) का उपयोग करता है।
Ethereum में स्टोरेज स्लॉट्स प्रभावी रूप से एक बहुत बड़ा key-value स्टोर होते हैं:
{
key: [smart_contract_address, storage slot]
value: 32_byte_slot // (for example: 0x00)
}
Solana का मॉडल भी इसी तरह का है: यह एक बहुत बड़ा key-value स्टोर है जहाँ “key” एक base 58 एनकोडेड एड्रेस है और value एक डेटा ब्लॉब (data blob) है जो 10MB तक बड़ा हो सकता है (या वैकल्पिक रूप से इसमें कुछ भी नहीं हो सकता है)। इसे इस प्रकार देखा जा सकता है:
{
// key is a base58 encoded 32 byte sequence
key: ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs
value: {
data: 020000006ad1897139ac2bdb67a3c66a...
// other fields are omitted
}
}
Ethereum में, एक स्मार्ट कॉन्ट्रैक्ट का बाइटकोड और एक स्मार्ट कॉन्ट्रैक्ट के स्टोरेज वेरिएबल्स को अलग-अलग स्टोर किया जाता है, यानी उन्हें अलग-अलग इंडेक्स किया जाता है और अलग-अलग APIs का उपयोग करके लोड किया जाना चाहिए।
निम्नलिखित चित्र दिखाता है कि Ethereum स्टेट (state) को कैसे मेंटेन करता है। प्रत्येक अकाउंट एक Merkle tree में एक लीफ (leaf) होता है। ध्यान दें कि “storage variables” स्मार्ट कॉन्ट्रैक्ट (Account 1) के अकाउंट के “अंदर” स्टोर होते हैं।

Solana में, सब कुछ एक अकाउंट है जो संभावित रूप से डेटा होल्ड कर सकता है। कभी-कभी हम एक अकाउंट को “program account” या दूसरे अकाउंट को “storage account” कहते हैं, लेकिन एकमात्र अंतर यह है कि क्या executable फ्लैग true पर सेट है और हम अकाउंट के डेटा फील्ड का उपयोग कैसे करना चाहते हैं।
नीचे, हम देख सकते हैं कि Solana स्टोरेज, Solana एड्रेसेस से लेकर एक अकाउंट तक एक विशाल key-value स्टोर है:

कल्पना करें कि यदि Ethereum में कोई स्टोरेज वेरिएबल्स न हों और स्मार्ट कॉन्ट्रैक्ट्स डिफ़ॉल्ट रूप से mutable (परिवर्तनीय) हों। डेटा स्टोर करने के लिए, आपको अन्य “स्मार्ट कॉन्ट्रैक्ट्स” बनाने होंगे और डेटा को उनके बाइटकोड में रखना होगा, फिर आवश्यकता पड़ने पर इसे संशोधित करना होगा। यह Solana का एक मेंटल मॉडल (mental model) है।
इसका एक और मेंटल मॉडल यह है कि Unix में सब कुछ एक फाइल कैसे है, बस कुछ फाइलें executable होती हैं। Solana अकाउंट्स को फाइलों के रूप में सोचा जा सकता है। वे कंटेंट होल्ड करते हैं, लेकिन उनके पास मेटाडेटा (metadata) भी होता है जो दर्शाता है कि फ़ाइल का मालिक कौन है, क्या यह executable है, और इसी तरह की अन्य जानकारियाँ।
Ethereum में, स्टोरेज वेरिएबल्स सीधे स्मार्ट कॉन्ट्रैक्ट से जुड़े होते हैं। जब तक कोई स्मार्ट कॉन्ट्रैक्ट पब्लिक वेरिएबल्स, delegatecall, या किसी सेटर (setter) मेथड के माध्यम से राइट (write) या रीड (read) एक्सेस नहीं देता है, डिफ़ॉल्ट रूप से, एक स्टोरेज वेरिएबल को केवल एक ही कॉन्ट्रैक्ट द्वारा लिखा या पढ़ा जा सकता है (हालाँकि कोई भी स्टोरेज वेरिएबल्स को ऑफ-चेन पढ़ सकता है)। Solana में, सभी “storage variables” को किसी भी प्रोग्राम द्वारा पढ़ा जा सकता है, लेकिन केवल इसका ओनर (owner) प्रोग्राम ही इसमें लिख सकता है।
जिस तरह से स्टोरेज को एक प्रोग्राम से “जोड़ा जाता” है, वह owner फील्ड के माध्यम से होता है।
नीचे दी गई छवि में, हम देखते हैं कि अकाउंट B, प्रोग्राम अकाउंट A के स्वामित्व (owned) में है। हम जानते हैं कि A एक प्रोग्राम अकाउंट है क्योंकि “executable” को true पर सेट किया गया है। यह दर्शाता है कि B का डेटा फील्ड A के लिए डेटा स्टोर कर रहा होगा:

Solana प्रोग्राम्स को उपयोग करने से पहले initialize किया जाना चाहिए
Ethereum में, हम सीधे एक स्टोरेज वेरिएबल में लिख सकते हैं जिसका हमने पहले उपयोग नहीं किया है। हालाँकि, Solana प्रोग्राम्स को एक स्पष्ट इनिशियलाइज़ेशन (initialization) ट्रांज़ैक्शन की आवश्यकता होती है। यानी, हमें डेटा लिखने से पहले अकाउंट बनाना होगा।
एक ही ट्रांज़ैक्शन में Solana अकाउंट को initialize करना और उसमें लिखना संभव है — हालाँकि इससे सुरक्षा संबंधी समस्याएँ (security issues) उत्पन्न होती हैं जो चर्चा को जटिल बना देंगी यदि हम अभी उनसे निपटते हैं। अभी के लिए, इतना कहना पर्याप्त है कि Solana अकाउंट्स को उपयोग करने से पहले initialize किया जाना चाहिए।
एक बेसिक स्टोरेज का उदाहरण
आइए निम्नलिखित Solidity कोड को Solana में बदलें (translate करें):
contract BasicStorage {
Struct MyStorage {
uint64 x;
}
MyStorage public myStorage;
function set(uint64 _x) external {
myStorage.x = _x;
}
}
यह अजीब लग सकता है कि हमने एक सिंगल वेरिएबल को एक स्ट्रक्ट (struct) में रैप किया है।
लेकिन Solana प्रोग्राम्स में, विशेष रूप से Anchor में, सभी स्टोरेज, या यों कहें कि अकाउंट डेटा, को एक स्ट्रक्ट (struct) के रूप में माना जाता है। इसका कारण अकाउंट डेटा की फ्लेक्सिबिलिटी (flexibility) है। चूँकि अकाउंट्स डेटा ब्लॉब्स होते हैं जो काफी बड़े (10MB तक) हो सकते हैं, इसलिए हमें डेटा की व्याख्या (interpret) करने के लिए कुछ “structure” की आवश्यकता होती है, अन्यथा यह बिना किसी अर्थ के केवल बाइट्स का एक क्रम (sequence) होता है।
पर्दे के पीछे, जब हम डेटा को पढ़ने या लिखने का प्रयास करते हैं, तो Anchor अकाउंट डेटा को स्ट्रक्ट्स में deserialize और serialize करता है।
जैसा कि ऊपर उल्लेख किया गया है, हमें Solana अकाउंट का उपयोग करने से पहले उसे initialize करने की आवश्यकता है, इसलिए set() फ़ंक्शन को लागू करने से पहले, हमें initialize() फ़ंक्शन लिखना होगा।
Account इनिशियलाइज़ेशन बॉयलरप्लेट (boilerplate) कोड
आइए basic_storage नामक एक नया Anchor प्रोजेक्ट बनाएँ।
नीचे हमने एक MyStorage स्ट्रक्ट को initialize करने के लिए न्यूनतम कोड लिखा है, जो केवल एक संख्या, x को होल्ड करता है। (कोड के निचले भाग में स्ट्रक्ट MyStorage देखें):
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");
#[program]
pub mod basic_storage {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init,
payer = signer,
space=size_of::<MyStorage>() + 8,
seeds = [],
bump)]
pub my_storage: Account<'info, MyStorage>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyStorage {
x: u64,
}
1) initialize फ़ंक्शन
ध्यान दें कि initialize() फ़ंक्शन में कोई कोड नहीं है — वास्तव में यह केवल Ok(()) रिटर्न करता है:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
यह अनिवार्य नहीं है कि अकाउंट्स को initialize करने वाले फ़ंक्शंस खाली (empty) हों, हमारे पास कस्टम लॉजिक हो सकता है। लेकिन हमारे उदाहरण के लिए, यह खाली है। यह भी अनिवार्य नहीं है कि अकाउंट्स को initialize करने वाले फ़ंक्शंस को initialize ही कहा जाए, लेकिन यह एक उपयोगी नाम है।
2) Initialize स्ट्रक्ट
Initialize स्ट्रक्ट में किसी अकाउंट को initialize करने के लिए आवश्यक संसाधनों (resources) के संदर्भ (references) होते हैं:
my_storage:MyStorageप्रकार का एक स्ट्रक्ट जिसे हम initialize कर रहे हैं।signer: वह वॉलेट जो स्ट्रक्ट के स्टोरेज के लिए “गैस (gas)” का भुगतान कर रहा है। (स्टोरेज के लिए गैस की लागत पर बाद में चर्चा की गई है)।system_program: हम इस ट्यूटोरियल में बाद में इस पर चर्चा करेंगे।

'info कीवर्ड एक Rust lifetime है। यह एक बड़ा विषय है और अभी के लिए इसे बॉयलरप्लेट (boilerplate) मानना सबसे अच्छा है।
हम my_storage के ऊपर वाले मैक्रो (macro) पर ध्यान केंद्रित करेंगे, क्योंकि यहीं इनिशियलाइज़ेशन का मुख्य कार्य हो रहा है।
3) Initialize स्ट्रक्ट में my_storage फील्ड
my_storage फील्ड के ऊपर का एट्रिब्यूट मैक्रो (बैंगनी तीर (purple arrow)) ही वह तरीका है जिससे Anchor को पता चलता है कि यह ट्रांज़ैक्शन इस अकाउंट को initialize करने के लिए है (याद रखें, एक attribute-like macro # से शुरू होता है और स्ट्रक्ट को अतिरिक्त कार्यक्षमता के साथ बढ़ाता है):

यहाँ महत्वपूर्ण कीवर्ड init है।
जब हम किसी अकाउंट को init करते हैं, तो हमें अतिरिक्त जानकारी देनी होती है:
payer(नीला बॉक्स (blue box)): स्टोरेज आवंटित करने के लिए SOL का भुगतान कौन कर रहा है। साइनर (signer) कोmutके रूप में निर्दिष्ट किया गया है क्योंकि उनका अकाउंट बैलेंस बदल जाएगा, यानी उनके अकाउंट से कुछ SOL काट लिए जाएंगे। इसलिए, हम उनके अकाउंट को “mutable” के रूप में एनोटेट करते हैं।space(नारंगी बॉक्स (orange box)): यह दर्शाता है कि अकाउंट कितनी जगह (space) लेगा। इसे खुद पता लगाने के बजाय, हमstd::mem::size_ofउपयोगिता (utility) का उपयोग कर सकते हैं और उस स्ट्रक्ट का उपयोग कर सकते हैं जिसे हम स्टोर करने का प्रयास कर रहे हैं:MyStorage(हरा बॉक्स (green box)), एक तर्क (argument) के रूप में।+ 8(गुलाबी बॉक्स (pink box)) के बारे में हम अगले बिंदु में चर्चा करेंगे।seedsऔरbump(लाल बॉक्स (red box)): एक प्रोग्राम के पास कई अकाउंट्स हो सकते हैं, यह “seed” के साथ अकाउंट्स के बीच “भेद (discriminates)” करता है जिसका उपयोग “discriminator” की गणना करने में किया जाता है। “discriminator” 8 बाइट्स लेता है, यही कारण है कि हमें अपने स्ट्रक्ट द्वारा ली जाने वाली जगह के अलावा अतिरिक्त 8 बाइट्स आवंटित करने की आवश्यकता होती है। बंप (bump) को अभी के लिए बॉयलरप्लेट माना जा सकता है।
यह समझने में थोड़ा ज्यादा लग सकता है, चिंता न करें। किसी अकाउंट को initialize करने को काफी हद तक अभी के लिए बॉयलरप्लेट के रूप में माना जा सकता है।
4) system program क्या है?
system program Solana रनटाइम (runtime) में निर्मित एक प्रोग्राम है (कुछ हद तक Ethereum precompile की तरह) जो एक अकाउंट से दूसरे अकाउंट में SOL ट्रांसफर करता है। हम बाद के एक ट्यूटोरियल में SOL ट्रांसफर करने के बारे में फिर से चर्चा करेंगे। अभी के लिए, हमें साइनर से SOL ट्रांसफर करने की आवश्यकता है, जो MyStruct स्टोरेज के लिए भुगतान कर रहा है, इसलिए system program हमेशा इनिशियलाइज़ेशन ट्रांज़ैक्शन का एक हिस्सा होता है।
5) MyStorage स्ट्रक्ट
Solana अकाउंट के अंदर डेटा फील्ड को याद करें:

पर्दे के पीछे (Under the hood), यह एक बाइट सीक्वेंस (byte sequence) है। ऊपर दिए गए उदाहरण में स्ट्रक्ट:
#[account]
pub struct MyStorage {
x: u64,
}
जब इसमें लिखा जाता है, तो यह एक बाइट सीक्वेंस में serialize हो जाता है और data फील्ड में स्टोर हो जाता है। लिखने के दौरान, data फील्ड को उस स्ट्रक्ट के अनुसार deserialize किया जाता है।
हमारे उदाहरण में, हम स्ट्रक्ट में केवल एक वेरिएबल का उपयोग कर रहे हैं, हालाँकि अगर हम चाहें तो और भी जोड़ सकते हैं, या किसी अन्य प्रकार (type) के वेरिएबल्स जोड़ सकते हैं।
Solana रनटाइम हमें डेटा स्टोर करने के लिए स्ट्रक्ट्स का उपयोग करने के लिए मजबूर नहीं करता है। Solana के दृष्टिकोण से, अकाउंट केवल एक डेटा ब्लॉब (data blob) होल्ड करता है। हालाँकि, Rust के पास स्ट्रक्ट्स को डेटा ब्लॉब्स में और इसके विपरीत बदलने के लिए बहुत सी सुविधाजनक लाइब्रेरीज़ हैं, इसलिए स्ट्रक्ट्स का उपयोग एक परिपाटी (convention) है। Anchor पर्दे के पीछे इन लाइब्रेरीज़ का लाभ उठा रहा है।
Solana अकाउंट्स का उपयोग करने के लिए आपको स्ट्रक्ट्स का उपयोग करने की आवश्यकता नहीं है। बाइट्स के क्रम (sequence) को सीधे लिखना संभव है, लेकिन यह डेटा स्टोर करने का एक सुविधाजनक तरीका नहीं है।
#[account] मैक्रो यह सारा जादू पारदर्शी (transparently) रूप से लागू करता है।
6) यूनिट टेस्ट (Unit test) इनिशियलाइज़ेशन
निम्नलिखित Typescript कोड ऊपर दिए गए Rust कोड को चलाएगा।
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";
describe("basic_storage", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.BasicStorage as Program<BasicStorage>;
it("Is initialized!", async () => {
const seeds = []
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("the storage account address is", myStorage.toBase58());
await program.methods.initialize().accounts({ myStorage: myStorage }).rpc();
});
});
यहाँ यूनिट टेस्ट का आउटपुट दिया गया है:

हम इसके बारे में अगले ट्यूटोरियल में अधिक जानेंगे, लेकिन Solana के लिए यह आवश्यक है कि हम पहले से उन अकाउंट्स को निर्दिष्ट (specify) करें जिनके साथ कोई ट्रांज़ैक्शन इंटरैक्ट करेगा। चूँकि हम उस अकाउंट के साथ इंटरैक्ट कर रहे हैं जो MyStruct को स्टोर करता है, हमें इसके “एड्रेस” की पहले से गणना करनी होगी और इसे initialize() फ़ंक्शन में पास करना होगा। यह निम्नलिखित Typescript कोड के साथ किया जाता है:
seeds = []
const [myStorage, _bump] =
anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
ध्यान दें कि seeds एक खाली ऐरे (empty array) है, ठीक वैसे ही जैसे यह Anchor प्रोग्राम में है।
Solana में अकाउंट एड्रेस की भविष्यवाणी करना Ethereum में create2 की तरह है
Ethereum में, create2 का उपयोग करके बनाए गए कॉन्ट्रैक्ट का एड्रेस निम्न पर निर्भर करता है:
- डिप्लॉय करने वाले कॉन्ट्रैक्ट का एड्रेस
- एक साल्ट (salt)
- और बनाए गए कॉन्ट्रैक्ट का बाइटकोड
Solana में initialize किए गए अकाउंट्स के एड्रेस की भविष्यवाणी (predicting) करना बहुत समान है सिवाय इसके कि यह “बाइटकोड” को नज़रअंदाज़ करता है। विशेष रूप से, यह निम्न पर निर्भर करता है:
- वह प्रोग्राम जो स्टोरेज अकाउंट का मालिक है,
basic_storage(जो डिप्लॉय करने वाले कॉन्ट्रैक्ट के एड्रेस के समान है) - और
seeds(जो create2 के “salt” के समान है)
इस ट्यूटोरियल के सभी उदाहरणों में, seeds एक खाली ऐरे है, लेकिन हम बाद के ट्यूटोरियल में गैर-खाली (non-empty) ऐरेज़ का पता लगाएंगे।
my_storage को myStorage में बदलना न भूलें
Anchor चुपचाप Rust के स्नेक केस (snake case) को Typescript के कैमल केस (camel case) में बदल देता है। जब हम Typescript में initialize फ़ंक्शन को .accounts({myStorage: myStorage}) प्रदान करते हैं, तो यह Rust में Initialize स्ट्रक्ट में my_storage की (key) को “भर (filling out)” रहा होता है (नीचे हरा घेरा (green circle))। system_program और Signer चुपचाप Anchor द्वारा भर दिए जाते हैं:

अकाउंट्स को दो बार initialize नहीं किया जा सकता
यदि हम किसी अकाउंट को फिर से initialize कर पाते, तो यह अत्यधिक समस्याग्रस्त होता क्योंकि कोई उपयोगकर्ता सिस्टम से डेटा मिटा सकता था! शुक्र है, Anchor बैकग्राउंड में इससे बचाव करता है।
यदि आप टेस्ट को दूसरी बार चलाते हैं (लोकल वैलिडेटर (local validator) को रीसेट किए बिना), तो आपको नीचे स्क्रीनशॉट में दी गई एरर (error) मिलेगी।
वैकल्पिक रूप से, यदि आप लोकल वैलिडेटर का उपयोग नहीं कर रहे हैं तो आप निम्नलिखित टेस्ट चला सकते हैं:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";
describe("basic_storage", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.BasicStorage as Program<BasicStorage>;
it("Is initialized!", async () => {
const seeds = []
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
// ********************************************
// **** NOTE THAT WE CALL INITIALIZE TWICE ****
// ********************************************
await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
});
});
जब हम टेस्ट चलाते हैं, तो टेस्ट विफल हो जाता है क्योंकि initialize को दूसरी कॉल एक एरर (error) फेंकती है। अपेक्षित (expected) आउटपुट इस प्रकार है:

यदि टेस्ट को कई बार चला रहे हैं तो वैलिडेटर को रीसेट करना न भूलें
चूँकि solana-test-validator अभी भी पहले यूनिट टेस्ट के अकाउंट को याद रखेगा, आप टेस्ट के बीच में solana-test-validator --reset का उपयोग करके वैलिडेटर को रीसेट करना चाहेंगे। अन्यथा, आपको ऊपर दी गई एरर मिलेगी।
अकाउंट्स को initialize करने का सारांश (Summary)
किसी अकाउंट को initialize करने की आवश्यकता संभवतः अधिकांश EVM डेवलपर्स को अप्राकृतिक (unnatural) लगेगी।
चिंता न करें, आप इस कोड सीक्वेंस को बार-बार देखेंगे, और कुछ समय बाद यह आपकी आदत (second nature) बन जाएगा।
हमने इस ट्यूटोरियल में केवल स्टोरेज को initialize करना देखा है, आने वाले ट्यूटोरियल में हम स्टोरेज को पढ़ना (reading), लिखना (writing), और हटाना (deleting) सीखेंगे। आज हमने जो भी कोड देखा है, वह क्या करता है, इसकी सहज समझ (intuitive grasp) प्राप्त करने के लिए भरपूर अवसर होंगे।
अभ्यास (Exercise): MyStorage को x और y होल्ड करने के लिए इस प्रकार संशोधित करें जैसे कि यह एक कार्टेशियन कोऑर्डिनेट (cartesian coordinate) हो। इसका अर्थ है MyStorage स्ट्रक्ट में y जोड़ना और उन्हें u64 से i64 में बदलना। आपको कोड के अन्य हिस्सों को संशोधित करने की आवश्यकता नहीं होगी क्योंकि size_of आपके लिए आकार की फिर से गणना करेगा। वैलिडेटर को रीसेट करना सुनिश्चित करें ताकि मूल स्टोरेज अकाउंट मिट जाए और आपको अकाउंट को फिर से initialize करने से रोका न जाए।
RareSkills के साथ और जानें
अधिक जानने के लिए हमारा Solana कोर्स देखें।
मूल रूप से 24 फरवरी, 2024 को प्रकाशित