Solana में किसी account का owner SOL balance को कम करने, account में डेटा लिखने (write data) और owner को बदलने में सक्षम होता है।
यहाँ Solana में account ownership का सारांश दिया गया है:
system programउन wallets और keypair accounts का मालिक (owner) होता है, जिनकी ownership किसी program को assign नहीं की गई है (initialized नहीं हुए हैं)।- BPFLoader programs का मालिक होता है।
- एक program Solana PDAs का मालिक होता है। यह keypair accounts का भी मालिक हो सकता है यदि ownership को program में transfer कर दिया गया हो (यही initialization के दौरान होता है)।
अब हम इन तथ्यों के निहितार्थ (implications) की जांच करेंगे।
system program keypair accounts का मालिक होता है
इसे स्पष्ट करने के लिए, आइए Solana CLI का उपयोग करके अपने Solana wallet address को देखें और इसके metadata का निरीक्षण करें:

ध्यान दें कि owner हमारा address नहीं है, बल्कि 111...111 address वाला एक account है। यह system program है, वही system program जो SOL को एक जगह से दूसरी जगह भेजता है जैसा कि हमने पिछले tutorials में देखा था।
केवल एक account के owner के पास ही उसमें मौजूद डेटा को modify करने की क्षमता होती है
इसमें lamport डेटा को कम करना शामिल है (आपको किसी अन्य account के lamport डेटा को बढ़ाने के लिए owner होने की आवश्यकता नहीं है, जैसा कि हम बाद में देखेंगे)।
हालांकि आप किसी वैचारिक अर्थ में अपने wallet के “मालिक” हैं, लेकिन आपके पास सीधे इसमें डेटा लिखने या lamport balance को कम करने की क्षमता नहीं है क्योंकि, Solana runtime के दृष्टिकोण से, आप इसके owner नहीं हैं।
आप अपने wallet में SOL इसलिए खर्च कर पाते हैं क्योंकि आपके पास वह private key है जिसने address, या public key जनरेट की है। जब system program यह पहचान लेता है कि आपने public key के लिए एक valid signature प्रस्तुत किया है, तो वह account में lamports खर्च करने के आपके request को वैध (legitimate) मान लेगा, और फिर आपके instructions के अनुसार उन्हें खर्च करेगा।
हालाँकि, system program किसी signer को सीधे account में डेटा लिखने के लिए कोई mechanism प्रदान नहीं करता है।
ऊपर दिए गए उदाहरण में दिखाया गया account एक keypair account है, या जिसे हम एक “regular Solana wallet” मान सकते हैं। system program इन keypair accounts का owner होता है।
Programs द्वारा initialize किए गए PDAs और keypair accounts के मालिक program होते हैं
Programs उन PDAs या keypair accounts में डेटा लिख (write) सकते हैं जो program के बाहर बनाए गए थे लेकिन program द्वारा initialize किए गए थे, इसका कारण यह है कि program उनका मालिक होता है।
जब हम re-initialization attack पर चर्चा करेंगे तो हम initialization को और अधिक गहराई से समझेंगे, लेकिन अभी के लिए, महत्वपूर्ण बात यह है कि किसी account को initialize करने से उस account का owner system program से बदलकर वह program हो जाता है जो उसे initialize कर रहा है।
इसे स्पष्ट करने के लिए, निम्नलिखित program पर विचार करें जो एक PDA और एक keypair account को initialize करता है। Typescript test initialization transaction से पहले और बाद में owner को console log करेगा।
यदि हम किसी ऐसे address के owner का पता लगाने का प्रयास करते हैं जो मौजूद नहीं है, तो हमें एक null मिलता है।
यहाँ Rust कोड है:
use anchor_lang::prelude::*;
declare_id!("C2ZKJPhNiCM6CqTneGUXJoE4o6YhMzNUes3q5WNcH3un");
#[program]
pub mod owner {
use super::*;
pub fn initialize_keypair(ctx: Context<InitializeKeypair>) -> Result<()> {
Ok(())
}
pub fn initialize_pda(ctx: Context<InitializePda>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeKeypair<'info> {
#[account(init, payer = signer, space = 8)]
keypair: Account<'info, Keypair>,
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct InitializePda<'info> {
#[account(init, payer = signer, space = 8, seeds = [], bump)]
pda: Account<'info, Pda>,
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
#[account]
pub struct Keypair();
#[account]
pub struct Pda();
यहाँ Typescript कोड है:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Owner } from "../target/types/owner";
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("owner", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Owner as Program<Owner>;
it("Is initialized!", async () => {
console.log("program address", program.programId.toBase58());
const seeds = []
const [pda, bump_] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("owner of pda before initialize:",
await anchor.getProvider().connection.getAccountInfo(pda));
await program.methods.initializePda().accounts({pda: pda}).rpc();
console.log("owner of pda after initialize:",
(await anchor.getProvider().connection.getAccountInfo(pda)).owner.toBase58());
let keypair = anchor.web3.Keypair.generate();
console.log("owner of keypair before airdrop:",
await anchor.getProvider().connection.getAccountInfo(keypair.publicKey));
await airdropSol(keypair.publicKey, 1); // 1 SOL
console.log("owner of keypair after airdrop:",
(await anchor.getProvider().connection.getAccountInfo(keypair.publicKey)).owner.toBase58());
await program.methods.initializeKeypair()
.accounts({keypair: keypair.publicKey})
.signers([keypair]) // the signer must be the keypair
.rpc();
console.log("owner of keypair after initialize:",
(await anchor.getProvider().connection.getAccountInfo(keypair.publicKey)).owner.toBase58());
});
});
Tests इस प्रकार काम करते हैं:
- यह PDA के address का अनुमान लगाता है और owner को query करता है। इसे
nullमिलता है। - यह
initializePDAको कॉल करता है और फिर owner को query करता है। इसे program का address मिलता है। - यह एक keypair account जनरेट करता है और owner को query करता है। इसे
nullमिलता है। - यह keypair account में SOL airdrop करता है। अब owner
system programहै, बिल्कुल एक सामान्य wallet की तरह। - यह
initializeKeypairको कॉल करता है और फिर owner को query करता है। इसे program का address मिलता है।
Test result का स्क्रीनशॉट नीचे दिया गया है:

इस प्रकार program accounts में डेटा लिखने में सक्षम होता है: वह उनका मालिक होता है। Initialization के दौरान, program account पर ownership ले लेता है।
Exercise: keypair और pda के address को प्रिंट करने के लिए test को modify करें। फिर यह निरीक्षण करने के लिए Solana CLI का उपयोग करें कि उन accounts का owner कौन है। यह test द्वारा प्रिंट किए गए आउटपुट से मेल खाना चाहिए। सुनिश्चित करें कि बैकग्राउंड में solana-test-validator चल रहा है ताकि आप CLI का उपयोग कर सकें।
BPFLoaderUpgradeable programs का मालिक होता है
आइए अपने program के owner का पता लगाने के लिए Solana CLI का उपयोग करें:

जिस wallet ने program को deploy किया है, वह उसका मालिक (owner) नहीं है। Solana programs को deploy करने वाले wallet द्वारा upgrade किए जा सकने का कारण यह है कि BPFLoaderUpgradeable program में नया bytecode लिखने में सक्षम है, और यह केवल एक पूर्व-निर्धारित (predesignated) address से ही नया bytecode स्वीकार करेगा: वह address जिसने मूल रूप से program को deploy किया था।
जब हम किसी program को deploy (या upgrade) करते हैं, तो हम वास्तव में BPFLoaderUpgradeable program को एक कॉल कर रहे होते हैं, जैसा कि logs में देखा जा सकता है:
Signature: 2zBBEPWsMvf8t7wkNEDqfHJKw83aBMgwGi3G9uZ6m9qG9t4kjJA2wFEP84dkKCjiCdbh54xeEDYFeDcNS7FkyLEw
Status: Ok
Log Messages:
Program 11111111111111111111111111111111 invoke [1]
Program 11111111111111111111111111111111 success
Program BPFLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Deployed program C2ZKJPhNiCM6CqTneGUXJoE4o6YhMzNUes3q5WNcH3un
Program BPFLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 34:
Programs अपने owned accounts की ownership transfer कर सकते हैं
यह एक ऐसा फीचर है जिसका आप शायद बहुत अधिक उपयोग नहीं करेंगे, लेकिन इसे करने के लिए यहाँ कोड दिया गया है।
Rust:
use anchor_lang::prelude::*;
use std::mem::size_of;
use anchor_lang::system_program;
declare_id!("Hxj38tktrD7YcSvKRxVrYQfxptkZd7NVbmrRKvLxznyA");
#[program]
pub mod change_owner {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn change_owner(ctx: Context<ChangeOwner>) -> Result<()> {
let account_info = &mut ctx.accounts.my_storage.to_account_info();
// assign is the function to transfer ownership
account_info.assign(&system_program::ID);
// we must erase all the data in the account or the transfer will fail
let res = account_info.realloc(0, false);
if !res.is_ok() {
return err!(Err::ReallocFailed);
}
Ok(())
}
}
#[error_code]
pub enum Err {
#[msg("realloc failed")]
ReallocFailed,
}
#[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>,
}
#[derive(Accounts)]
pub struct ChangeOwner<'info> {
#[account(mut)]
pub my_storage: Account<'info, MyStorage>,
}
#[account]
pub struct MyStorage {
x: u64,
}
Typescript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ChangeOwner } from "../target/types/change_owner";
import privateKey from '/Users/jeffreyscholz/.config/solana/id.json';
describe("change_owner", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.ChangeOwner as Program<ChangeOwner>;
it("Is initialized!", async () => {
const deployer = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(privateKey));
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();
await program.methods.changeOwner().accounts({myStorage: myStorage}).rpc();
// after the ownership has been transferred
// the account can still be initialized again
await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
});
});
यहाँ कुछ चीजें हैं जिनकी ओर हम ध्यान आकर्षित करना चाहते हैं:
- Account transfer करने के बाद, उसी transaction में डेटा को मिटा दिया जाना चाहिए (erased)। अन्यथा, हम अन्य programs के owned accounts में डेटा insert कर सकते हैं। यह
account_info.realloc(0, false);कोड के द्वारा होता है।falseका अर्थ है डेटा को शून्य (zero out) न करें, लेकिन इससे कोई फर्क नहीं पड़ता क्योंकि अब वहां कोई डेटा नहीं है। - Account ownership transfer करने से account स्थायी रूप से नहीं हटता है, इसे फिर से initialize किया जा सकता है जैसा कि tests दिखाते हैं।
अब जब हम स्पष्ट रूप से समझ गए हैं कि programs उनके द्वारा initialize किए गए PDAs और keypair accounts के मालिक होते हैं, तो हम जो दिलचस्प और उपयोगी काम कर सकते हैं वह है उनमें से SOL को बाहर transfer करना।
एक PDA से SOL Transfer करना: Crowdfund का उदाहरण
नीचे हम एक साधारण (barebones) crowdfunding ऐप का कोड दिखा रहे हैं। इसमें ध्यान देने योग्य फ़ंक्शन withdraw है जहाँ program, PDA से lamports को निकालकर withdrawer को transfer करता है।
use anchor_lang::prelude::*;
use anchor_lang::system_program;
use std::mem::size_of;
use std::str::FromStr;
declare_id!("BkthFL8LV2V2MxVgQtA9tT5goeeJhUdxRPahzavqHPFZ");
#[program]
pub mod crowdfund {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let initialized_pda = &mut ctx.accounts.pda;
Ok(())
}
pub fn donate(ctx: Context<Donate>, amount: u64) -> Result<()> {
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.signer.to_account_info().clone(),
to: ctx.accounts.pda.to_account_info().clone(),
},
);
system_program::transfer(cpi_context, amount)?;
Ok(())
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
ctx.accounts.pda.sub_lamports(amount)?;
ctx.accounts.signer.add_lamports(amount)?;
// in anchor 0.28 or lower, use the following syntax:
// **ctx.accounts.pda.to_account_info().try_borrow_mut_lamports()? -= amount;
// **ctx.accounts.signer.to_account_info().try_borrow_mut_lamports()? += amount;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(init, payer = signer, space=size_of::<Pda>() + 8, seeds=[], bump)]
pub pda: Account<'info, Pda>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Donate<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(mut)]
pub pda: Account<'info, Pda>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut, address = Pubkey::from_str("5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj").unwrap())]
pub signer: Signer<'info>,
#[account(mut)]
pub pda: Account<'info, Pda>,
}
#[account]
pub struct Pda {}
चूँकि program PDA का मालिक है, यह सीधे account से lamport balance काट (deduct) सकता है।
जब हम एक सामान्य wallet transaction के रूप में SOL transfer करते हैं, तो हम lamport balance को सीधे नहीं काटते हैं क्योंकि हम account के मालिक नहीं हैं। system program wallet का मालिक होता है, और यदि यह ऐसा करने का अनुरोध करने वाले transaction पर एक valid signature देखता है तो यह lamport balance को काट लेगा।
इस मामले में, program PDA का मालिक है, और इसलिए यह सीधे इसमें से lamports काट सकता है।
कोड में ध्यान देने योग्य कुछ अन्य बातें:
- हमने
#[account(mut, address = Pubkey::from_str("5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj").unwrap())]constraint का उपयोग करके यह hardcode किया है कि PDA से कौन withdraw कर सकता है। यह जांचता है कि उस account का address स्ट्रिंग में दिए गए address से मेल खाता है या नहीं। इस कोड को काम करने के लिए, हमेंuse std::str::FromStr;भी import करना पड़ा। इस कोड को test करने के लिए, स्ट्रिंग में दिए गए address को अपनेsolana addressसे बदल दें। - Anchor 0.29 के साथ, हम
ctx.accounts.pda.sub_lamports(amount)?;औरctx.accounts.signer.add_lamports(amount)?;सिंटैक्स का उपयोग कर सकते हैं। Anchor के पुराने वर्ज़न के लिए,ctx.accounts.pda.to_account_info().try_borrow_mut_lamports()? -= amount;औरctx.accounts.signer.to_account_info().try_borrow_mut_lamports()? += amount;का उपयोग करें। - जिस account में आप lamports transfer कर रहे हैं, उसका मालिक (owner) होना आपके लिए आवश्यक नहीं है।
यहाँ इसके साथ का Typescript कोड है:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Crowdfund } from "../target/types/crowdfund";
describe("crowdfund", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Crowdfund as Program<Crowdfund>;
it("Is initialized!", async () => {
const programId = await program.account.pda.programId;
let seeds = [];
let pdaAccount = anchor.web3.PublicKey.findProgramAddressSync(seeds, programId)[0];
const tx = await program.methods.initialize().accounts({
pda: pdaAccount
}).rpc();
// transfer 2 SOL
const tx2 = await program.methods.donate(new anchor.BN(2_000_000_000)).accounts({
pda: pdaAccount
}).rpc();
console.log("lamport balance of pdaAccount",
await anchor.getProvider().connection.getBalance(pdaAccount));
// transfer back 1 SOL
// the signer is the permitted address
await program.methods.withdraw(new anchor.BN(1_000_000_000)).accounts({
pda: pdaAccount
}).rpc();
console.log("lamport balance of pdaAccount",
await anchor.getProvider().connection.getBalance(pdaAccount));
});
});
Exercise: PDA से जितने lamports आप withdraw कर रहे हैं, उससे अधिक lamports receiving address में जोड़ने का प्रयास करें। यानी कोड को निम्नलिखित में बदलें:
ctx.accounts.pda.sub_lamports(amount)?;
// sneak in an extra lamport
ctx.accounts.signer.add_lamports(amount + 1)?;
Runtime आपको ब्लॉक कर देगा।
ध्यान दें कि lamport balance को rent-exempt threshold से नीचे withdraw करने पर account बंद (close) हो जाएगा। यदि account में डेटा है, तो वह मिट (erase) जाएगा। इसलिए, programs को SOL withdraw करने से पहले यह track करना चाहिए कि rent exemption के लिए कितने SOL की आवश्यकता है, जब तक कि उन्हें account के मिट जाने से कोई फर्क न पड़ता हो।
RareSkills के साथ और जानें
Topics की पूरी सूची के लिए हमारा Solana tutorial देखें।
मूल रूप से 7 मार्च, 2024 को प्रकाशित