पिछले ट्यूटोरियल्स में, हमें किसी अकाउंट में डेटा लिखने से पहले उसे एक अलग ट्रांज़ैक्शन में इनिशियलाइज़ करना पड़ता था। यूज़र के लिए चीज़ों को आसान बनाने के लिए हम यह चाह सकते हैं कि हम एक ही ट्रांज़ैक्शन में अकाउंट को इनिशियलाइज़ कर सकें और उसमें डेटा लिख सकें।
Anchor एक उपयोगी मैक्रो प्रदान करता है जिसे init_if_needed कहा जाता है, जो जैसा कि नाम से पता चलता है, अकाउंट मौजूद नहीं होने पर उसे इनिशियलाइज़ कर देगा।
नीचे दिए गए उदाहरण काउंटर (counter) को एक अलग इनिशियलाइज़ ट्रांज़ैक्शन की आवश्यकता नहीं है, यह सीधे counter स्टोरेज में “1” जोड़ना शुरू कर देगा।
Rust:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("9DbiqCqtqgP3NYufxBakbeRd7JyNpNYbsm6Jqrn8Z2Hn");
#[program]
pub mod init_if_needed {
use super::*;
pub fn increment(ctx: Context<Initialize>) -> Result<()> {
let current_counter = ctx.accounts.my_pda.counter;
ctx.accounts.my_pda.counter = current_counter + 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init_if_needed,
payer = signer,
space = size_of::<MyPDA>() + 8,
seeds = [],
bump
)]
pub my_pda: Account<'info, MyPDA>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyPDA {
pub counter: u64,
}
Typescript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { InitIfNeeded } from "../target/types/init_if_needed";
describe("init_if_needed", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.InitIfNeeded as Program<InitIfNeeded>;
it("Is initialized!", async () => {
const [myPda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);
await program.methods.increment().accounts({myPda: myPda}).rpc();
await program.methods.increment().accounts({myPda: myPda}).rpc();
await program.methods.increment().accounts({myPda: myPda}).rpc();
let result = await program.account.myPda.fetch(myPda);
console.log(`counter is ${result.counter}`);
});
});
जब हम anchor build के साथ इस प्रोग्राम को बिल्ड करने का प्रयास करते हैं, तो हमें निम्नलिखित त्रुटि मिलेगी:

init_if_needed requires that anchor-lang be imported with the init-if-needed cargo feature enabled त्रुटि को दूर करने के लिए, हम programs/<anchor_project_name> में Cargo.toml फ़ाइल खोल सकते हैं और निम्नलिखित लाइन जोड़ सकते हैं:
[dependencies]
anchor-lang = { version = "0.29.0", features = ["init-if-needed"] }
लेकिन इससे पहले कि हम सिर्फ त्रुटि को अनदेखा करें, हमें यह समझना चाहिए कि रीइनिशियलाइज़ेशन अटैक (re-initialization attack) क्या है और यह कैसे हो सकता है।
Anchor प्रोग्राम्स में, अकाउंट्स को दो बार इनिशियलाइज़ नहीं किया जा सकता है (डिफ़ॉल्ट रूप से)
यदि हम किसी ऐसे अकाउंट को इनिशियलाइज़ करने का प्रयास करते हैं जिसे पहले ही इनिशियलाइज़ किया जा चुका है, तो ट्रांज़ैक्शन विफल हो जाएगा।
Anchor को कैसे पता चलता है कि कोई अकाउंट पहले से ही इनिशियलाइज़्ड है?
Anchor के दृष्टिकोण से, यदि अकाउंट में शून्य लैम्पोर्ट बैलेंस (zero lamport balance) है या अकाउंट का स्वामित्व सिस्टम प्रोग्राम के पास है, तो यह इनिशियलाइज़ नहीं है।
सिस्टम प्रोग्राम के स्वामित्व वाले या शून्य लैम्पोर्ट बैलेंस वाले अकाउंट को फिर से इनिशियलाइज़ किया जा सकता है।
इसे स्पष्ट करने के लिए, हमारे पास सामान्य initialize फ़ंक्शन के साथ एक Solana प्रोग्राम है (जो init_if_needed का नहीं, बल्कि init का उपयोग करता है)। इसमें एक drain_lamports फ़ंक्शन और एक give_to_system_program फ़ंक्शन भी है, ये दोनों वही करते हैं जो इनके नाम सुझाते हैं:
use anchor_lang::prelude::*;
use std::mem::size_of;
use anchor_lang::system_program;
declare_id!("FC467mPCCKXG97ut1WdLLi55vuAcyCW8AD1vid27bZfn");
#[program]
pub mod reinit_attack {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn drain_lamports(ctx: Context<DrainLamports>) -> Result<()> {
let lamports = ctx.accounts.my_pda.to_account_info().lamports();
ctx.accounts.my_pda.sub_lamports(lamports)?;
ctx.accounts.signer.add_lamports(lamports)?;
Ok(())
}
pub fn give_to_system_program(ctx: Context<GiveToSystemProgram>) -> Result<()> {
let account_info = &mut ctx.accounts.my_pda.to_account_info();
// the assign method changes the owner
account_info.assign(&system_program::ID);
account_info.realloc(0, false)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct DrainLamports<'info> {
#[account(mut)]
pub my_pda: Account<'info, MyPDA>,
#[account(mut)]
pub signer: Signer<'info>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = signer, space = 8, seeds = [], bump)]
pub my_pda: Account<'info, MyPDA>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct GiveToSystemProgram<'info> {
#[account(mut)]
pub my_pda: Account<'info, MyPDA>,
}
#[account]
pub struct MyPDA {}
अब निम्नलिखित यूनिट टेस्ट पर विचार करें:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ReinitAttack } from "../target/types/reinit_attack";
describe("Program", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.ReinitAttack as Program<ReinitAttack>;
it("initialize after giving to system program or draining lamports", async () => {
const [myPda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);
await program.methods.initialize().accounts({myPda: myPda}).rpc();
await program.methods.giveToSystemProgram().accounts({myPda: myPda}).rpc();
await program.methods.initialize().accounts({myPda: myPda}).rpc();
console.log("account initialized after giving to system program!")
await program.methods.drainLamports().accounts({myPda: myPda}).rpc();
await program.methods.initialize().accounts({myPda: myPda}).rpc();
console.log("account initialized after draining lamports!")
});
});
इसका क्रम इस प्रकार है:
- हम PDA को इनिशियलाइज़ करते हैं
- हम PDA का स्वामित्व सिस्टम प्रोग्राम को ट्रांसफर करते हैं
- हम फिर से
initializeकॉल करते हैं, और यह सफल होता है - हम
my_pdaअकाउंट से लैम्पोर्ट्स खाली कर देते हैं - शून्य लैम्पोर्ट बैलेंस के साथ, Solana रनटाइम अकाउंट को गैर-मौजूद मानता है क्योंकि इसे हटाने के लिए शेड्यूल किया जाएगा क्योंकि यह अब रेंट एग्ज़ेम्प्ट (rent exempt) नहीं है।
- हम फिर से
initializeकॉल करते हैं, और यह सफल होता है। इस क्रम का पालन करने के बाद हमने सफलतापूर्वक अकाउंट को फिर से इनिशियलाइज़ कर लिया है।
फिर से बता दें, Solana में “initialized” फ़्लैग या ऐसा कुछ नहीं होता है। Anchor एक initialize ट्रांज़ैक्शन को सफल होने की अनुमति देगा यदि ओनर सिस्टम प्रोग्राम है या लैम्पोर्ट बैलेंस शून्य है।
हमारे उदाहरण में रीइनिशियलाइज़ेशन एक समस्या क्यों हो सकती है
सिस्टम प्रोग्राम को स्वामित्व ट्रांसफर करने के लिए अकाउंट के डेटा को मिटाने की आवश्यकता होती है। सभी लैम्पोर्ट्स को हटाना यह “संदेश” देता है कि आप नहीं चाहते कि अकाउंट आगे अस्तित्व में रहे।
क्या इन दोनों में से किसी भी कार्रवाई को करने से आपका इरादा काउंटर को फिर से शुरू करना है या काउंटर का जीवन समाप्त करना है? यदि आपका एप्लिकेशन कभी भी काउंटर के रीसेट होने की अपेक्षा नहीं करता है, तो इससे बग्स आ सकते हैं।
Anchor चाहता है कि आप इसके साथ अपने इरादे पर अच्छी तरह से विचार करें, यही कारण है कि यह आपको Cargo.toml में एक फीचर फ़्लैग सक्षम करने की अतिरिक्त प्रक्रिया से गुज़ारता है।
यदि आप इस बात से सहमत हैं कि काउंटर किसी बिंदु पर वापस रीसेट हो जाए और फिर से गिनना शुरू कर दे, तो रीइनिशियलाइज़ेशन कोई समस्या नहीं है। लेकिन यदि काउंटर किसी भी परिस्थिति में कभी शून्य पर रीसेट नहीं होना चाहिए, तो शायद आपके लिए यह बेहतर होगा कि आप initialization फ़ंक्शन को अलग से इम्प्लीमेंट करें और एक सुरक्षा उपाय जोड़ें ताकि यह सुनिश्चित हो सके कि इसे इसके जीवनकाल में केवल एक बार ही कॉल किया जा सके (उदाहरण के लिए, एक अलग अकाउंट में एक बुलियन फ़्लैग स्टोर करना)।
बेशक, आपके प्रोग्राम में अकाउंट को सिस्टम प्रोग्राम में ट्रांसफर करने या अकाउंट से लैम्पोर्ट निकालने का तंत्र आवश्यक रूप से नहीं हो सकता है। लेकिन Anchor के पास यह जानने का कोई तरीका नहीं है, इसलिए यह हमेशा init_if_needed के बारे में चेतावनी देता है क्योंकि यह निर्धारित नहीं कर सकता कि क्या अकाउंट वापस इनिशियलाइज़ करने योग्य स्थिति में जा सकता है।
दो इनिशियलाइज़ेशन पाथ होने से ऑफ़-बाय-वन (off-by-one) त्रुटि या अन्य आश्चर्यजनक व्यवहार हो सकता है
init_if_needed के साथ हमारे काउंटर उदाहरण में, काउंटर कभी भी शून्य के बराबर नहीं होता है क्योंकि पहला इनिशियलाइज़ेशन ट्रांज़ैक्शन भी मान को शून्य से एक तक बढ़ा देता है।
यदि हमारे पास एक और नियमित इनिशियलाइज़ेशन फ़ंक्शन होता जो काउंटर को नहीं बढ़ाता, तो काउंटर इनिशियलाइज़ हो जाता और उसका मान शून्य होता। यदि कोई बिज़नेस लॉजिक कभी शून्य मान वाले काउंटर की अपेक्षा नहीं करता है, तो अप्रत्याशित व्यवहार हो सकता है।
Ethereum में, उन वेरिएबल्स के स्टोरेज मान जिन्हें कभी “छुआ” नहीं गया है, उनका डिफ़ॉल्ट मान शून्य होता है। Solana में, जो अकाउंट इनिशियलाइज़ नहीं किए गए हैं वे शून्य-मान वाले वेरिएबल्स नहीं रखते हैं — उनका कोई अस्तित्व ही नहीं होता है और उन्हें पढ़ा नहीं जा सकता है।
Anchor में “Initialization” का अर्थ हमेशा “init” नहीं होता है
कुछ हद तक भ्रामक रूप से, कुछ लोग “initialize” शब्द का उपयोग Anchor के init मैक्रो की तुलना में अधिक सामान्य अर्थ में “पहली बार अकाउंट में डेटा लिखने” के लिए करते हैं।
यदि हम Soldev के उदाहरण प्रोग्राम को देखें, तो हम पाते हैं कि init मैक्रो का उपयोग नहीं किया गया है:
use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod initialization_insecure {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let mut user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
user.authority = ctx.accounts.authority.key();
user.serialize(&mut *ctx.accounts.user.data.borrow_mut())?;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
user: AccountInfo<'info>,
#[account(mut)]
authority: Signer<'info>,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
authority: Pubkey,
}
कोड लाइन 11 पर सीधे अकाउंट को पढ़ रहा है, फिर फ़ील्ड्स को सेट कर रहा है। प्रोग्राम आँख बंद करके डेटा को ओवरराइट कर देता है चाहे वह पहली बार लिख रहा हो या दूसरी बार (या तीसरी बार)।
इसके बजाय, यहाँ “initialize” के लिए शब्दावली है “अकाउंट में पहली बार लिखना”।
यहाँ “रीइनिशियलाइज़ेशन अटैक” उस चेतावनी से अलग प्रकार का है जिसके बारे में Anchor फ्रेमवर्क चेतावनी दे रहा है। विशेष रूप से, “initialize” को कई बार कॉल किया जा सकता है। Anchor का init मैक्रो जाँचता है कि लैम्पोर्ट बैलेंस शून्य-रहित है और प्रोग्राम के पास पहले से ही अकाउंट का स्वामित्व है, जो initialize के लिए कई कॉल्स को रोकेगा। init मैक्रो यह देख सकता है कि अकाउंट में पहले से ही लैम्पोर्ट्स हैं या उसका स्वामित्व प्रोग्राम के पास है। हालाँकि, उपरोक्त कोड में ऐसी कोई जाँच नहीं है।
रीइनिशियलाइज़ेशन अटैक के इस प्रकार को देखने के लिए उनके ट्यूटोरियल को पढ़ना उपयोगी है।
ध्यान दें कि इसमें Anchor के पुराने वर्ज़न का उपयोग किया गया है। AccountInfo, UncheckedAccount के लिए एक अन्य शब्द है, इसलिए आपको इसके ऊपर एक /// Check: कमेंट जोड़ना होगा।
अकाउंट डिस्क्रिमिनेटर (account discriminator) को मिटाने से अकाउंट फिर से इनिशियलाइज़ करने योग्य नहीं होगा
कोई अकाउंट इनिशियलाइज़ किया गया है या नहीं, इसका उसके अंदर मौजूद डेटा (या उसकी कमी) से कोई लेना-देना नहीं है।
किसी अकाउंट के डेटा को ट्रांसफर किए बिना उसे मिटाने के लिए:
use anchor_lang::prelude::*;
use std::mem::size_of;
use anchor_lang::system_program;
declare_id!("FC467mPCCKXG97ut1WdLLi55vuAcyCW8AD1vid27bZfn");
#[program]
pub mod reinit_attack {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn erase(ctx: Context<Erase>) -> Result<()> {
ctx.accounts.my_pda.realloc(0, false)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct Erase<'info> {
/// CHECK: We are going to erase the account
#[account(mut)]
pub my_pda: UncheckedAccount<'info>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = signer, space = 8, seeds = [], bump)]
pub my_pda: Account<'info, MyPDA>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyPDA {}
यह महत्वपूर्ण है कि हम UncheckedAccount का उपयोग करके डेटा मिटाएँ क्योंकि .realloc(0, false) किसी सामान्य Account पर उपलब्ध विधि नहीं है।
यह ऑपरेशन अकाउंट डिस्क्रिमिनेटर को मिटा देगा, इसलिए इसे अब Account के माध्यम से नहीं पढ़ा जा सकेगा।
अभ्यास: अकाउंट को इनिशियलाइज़ करें, erase को कॉल करें और फिर अकाउंट को फिर से इनिशियलाइज़ करने का प्रयास करें। यह विफल हो जाएगा क्योंकि भले ही अकाउंट में कोई डेटा नहीं है, फिर भी यह प्रोग्राम के स्वामित्व में है और इसमें शून्य-रहित लैम्पोर्ट बैलेंस (non-zero lamport balance) है।
सारांश
init_if_needed मैक्रो एक नए स्टोरेज अकाउंट के साथ इंटरैक्ट करने के लिए दो ट्रांज़ैक्शन की आवश्यकता से बचने के लिए सुविधाजनक हो सकता है। निम्नलिखित संभावित अवांछनीय स्थितियों पर विचार करने के लिए हमें बाध्य करने के लिए Anchor फ्रेमवर्क इसे डिफ़ॉल्ट रूप से ब्लॉक कर देता है:
- यदि लैम्पोर्ट बैलेंस को शून्य करने या सिस्टम प्रोग्राम को स्वामित्व ट्रांसफर करने की कोई विधि है, तो अकाउंट को फिर से इनिशियलाइज़ किया जा सकता है। बिज़नेस आवश्यकताओं के आधार पर यह एक समस्या हो भी सकती है और नहीं भी।
- यदि प्रोग्राम में
initमैक्रो औरinit_if_neededमैक्रो दोनों हैं, तो डेवलपर को यह सुनिश्चित करना चाहिए कि दो कोडपाथ होने से अप्रत्याशित स्टेट उत्पन्न न हो। - अकाउंट में डेटा पूरी तरह से मिट जाने के बाद भी, अकाउंट इनिशियलाइज़्ड ही रहता है।
- यदि प्रोग्राम में कोई फ़ंक्शन है जो “आँख बंद करके” अकाउंट में लिखता है, तो उस अकाउंट का डेटा ओवरराइट हो सकता है। इसके लिए आमतौर पर अकाउंट को
AccountInfoया इसके एलियासUncheckedAccountके माध्यम से लोड करने की आवश्यकता होती है।
RareSkills के साथ और जानें
हमारे बाकी Solana ट्यूटोरियल्स के लिए हमारा Solana development course देखें। पढ़ने के लिए धन्यवाद!
मूल रूप से 8 मार्च, 2024 को प्रकाशित