Solana Programming Model: Accounts, Programs, and PDAs
Learn Solana's unique programming model with accounts, programs, and Program Derived Addresses (PDAs).
Solana Programming Model: Accounts, Programs, and PDAs
Solana uses a unique account-based model different from Ethereum.
Key Concepts
Accounts
In Solana, everything is an account:
- Data Accounts: Store data
- Program Accounts: Store executable code
- System Accounts: Native programs
Account Structure
RUSTstruct AccountInfo { key: Pubkey, // Account address lamports: u64, // Balance in lamports (1 SOL = 1e9 lamports) data: Vec<u8>, // Account data owner: Pubkey, // Program that owns this account executable: bool, // Is this a program? rent_epoch: u64, // Rent epoch }
Programs
Programs are Solana's equivalent of smart contracts:
- Written in Rust (or C)
- Compiled to BPF (Berkeley Packet Filter) bytecode
- Stateless: Programs don't store data
- Data stored in separate accounts
Program Derived Addresses (PDAs)
PDAs are program-controlled addresses:
- Derived from program ID + seeds
- No private key (can't sign)
- Only the program can sign for them
- Used for program-owned accounts
Solana Program Example
RUSTuse solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, program_error::ProgramError, }; // Program entry point entrypoint!(process_instruction); pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { let accounts_iter = &mut accounts.iter(); // Get accounts let user_account = next_account_info(accounts_iter)?; let data_account = next_account_info(accounts_iter)?; // Verify ownership if data_account.owner != program_id { return Err(ProgramError::IncorrectProgramId); } // Read/write account data let mut data = data_account.data.borrow_mut(); // ... process instruction ... Ok(()) }
Key Differences from Ethereum
| Feature | Ethereum | Solana |
|---|---|---|
| State | In contract | In accounts |
| Execution | Sequential | Parallel |
| Fees | Gas | Compute units |
| Finality | ~15s | ~400ms |
| Throughput | ~15 TPS | ~65,000 TPS |
Account Ownership
RUST// Transfer ownership pub fn transfer_ownership( account: &AccountInfo, new_owner: &Pubkey, ) -> ProgramResult { // Only current owner can transfer account.assign(new_owner); Ok(()) } // Check ownership pub fn is_owned_by(account: &AccountInfo, owner: &Pubkey) -> bool { account.owner == owner }
PDA (Program Derived Address)
RUSTuse solana_program::pubkey::Pubkey; // Find PDA pub fn find_pda( program_id: &Pubkey, seeds: &[&[u8]], ) -> (Pubkey, u8) { Pubkey::find_program_address(seeds, program_id) } // Example: User's token account PDA let (user_token_pda, bump) = find_pda( &program_id, &[b"token", user_pubkey.as_ref()], );
Cross-Program Invocations (CPI)
Programs can call other programs:
RUSTuse solana_program::program::invoke; pub fn call_other_program( instruction: &Instruction, account_infos: &[AccountInfo], ) -> ProgramResult { invoke(instruction, account_infos)?; Ok(()) }
Real-World Examples
- Serum DEX: Decentralized exchange on Solana
- Raydium: AMM on Solana
- Magic Eden: NFT marketplace
- Jupiter: DEX aggregator
Advantages
- High Throughput: Parallel execution
- Low Fees: ~$0.00025 per transaction
- Fast Finality: Sub-second confirmation
- Composability: Programs can call each other
Challenges
- Account Model: Different from Ethereum
- Rent: Accounts must pay rent
- Size Limits: Accounts have size limits
- Learning Curve: Different programming model
Code Examples
Solana Account Structure
Basic account structure in Solana
// Simplified Solana account model
use std::collections::HashMap;
#[derive(Clone)]
pub struct Pubkey([u8; 32]);
struct Account {
key: Pubkey,
lamports: u64,
data: Vec<u8>,
owner: Pubkey,
executable: bool,
}
struct SolanaState {
accounts: HashMap<Pubkey, Account>,
}
impl SolanaState {
fn new() -> Self {
SolanaState {
accounts: HashMap::new(),
}
}
fn create_account(&mut self, key: Pubkey, owner: Pubkey, data: Vec<u8>) -> Account {
let account = Account {
key: key.clone(),
lamports: 0,
data,
owner,
executable: false,
};
self.accounts.insert(key, account.clone());
account
}
fn get_account(&self, key: &Pubkey) -> Option<&Account> {
self.accounts.get(key)
}
fn transfer_lamports(&mut self, from: &Pubkey, to: &Pubkey, amount: u64) -> Result<(), String> {
let from_account = self.accounts.get_mut(from)
.ok_or("From account not found")?;
if from_account.lamports < amount {
return Err("Insufficient balance".to_string());
}
from_account.lamports -= amount;
let to_account = self.accounts.get_mut(to)
.ok_or("To account not found")?;
to_account.lamports += amount;
Ok(())
}
}
fn main() {
let mut state = SolanaState::new();
let alice_key = Pubkey([1; 32]);
let bob_key = Pubkey([2; 32]);
// Create accounts
state.create_account(alice_key.clone(), Pubkey([0; 32]), vec![]);
state.create_account(bob_key.clone(), Pubkey([0; 32]), vec![]);
// Set initial balance
if let Some(account) = state.accounts.get_mut(&alice_key) {
account.lamports = 1_000_000_000; // 1 SOL
}
// Transfer
state.transfer_lamports(&alice_key, &bob_key, 100_000_000).unwrap();
println!("Solana account model: Everything is an account!");
}Explanation:
In Solana, everything is an account. Programs are accounts with executable=true. Data is stored in separate accounts. This enables parallel execution and high throughput.
Program Derived Address
PDA concept and usage
// Simplified PDA concept
use std::collections::HashMap;
struct Pubkey([u8; 32]);
struct PDA {
program_id: Pubkey,
seeds: Vec<Vec<u8>>,
address: Pubkey,
bump: u8,
}
fn find_pda(program_id: &Pubkey, seeds: &[&[u8]]) -> (Pubkey, u8) {
// Simplified: In real Solana, uses find_program_address
// Combines seeds + program_id + bump to find valid address
let combined: Vec<u8> = seeds.iter()
.flat_map(|s| s.iter().copied())
.chain(program_id.0.iter().copied())
.collect();
// Simplified hash
let mut address = [0u8; 32];
for (i, &byte) in combined.iter().take(32).enumerate() {
address[i] = byte;
}
(Pubkey(address), 255) // Simplified bump
}
struct Program {
id: Pubkey,
pdas: HashMap<Pubkey, PDA>,
}
impl Program {
fn new(id: Pubkey) -> Self {
Program {
id,
pdas: HashMap::new(),
}
}
fn create_pda(&mut self, seeds: &[&[u8]]) -> Pubkey {
let (address, bump) = find_pda(&self.id, seeds);
let pda = PDA {
program_id: self.id.clone(),
seeds: seeds.iter().map(|s| s.to_vec()).collect(),
address: address.clone(),
bump,
};
self.pdas.insert(address.clone(), pda);
address
}
}
fn main() {
let program_id = Pubkey([1; 32]);
let mut program = Program::new(program_id);
// Create PDA for user token account
let user_pubkey = b"user123";
let pda = program.create_pda(&[b"token", user_pubkey]);
println!("Created PDA for user token account");
println!("PDA can only be controlled by the program!");
}Explanation:
PDAs are addresses derived from a program ID and seeds. They have no private key, so only the program can sign for them. This enables programs to own accounts and manage state.
Exercises
Simple Account Transfer
Create a simple account transfer function!
Starter Code:
struct Account {
lamports: u64,
}
fn main() {
// Create two accounts
// Transfer lamports between them
}