# Native Rust Solana Programs Reference This reference covers native Rust-specific implementation patterns and workflows for building Solana programs without the Anchor framework. For general concepts (what PDAs/CPIs are), see the other reference files. ## Table of Contents - [Project Setup](#project-setup) - [Entrypoint Patterns](#entrypoint-patterns) - [Manual Account Handling](#manual-account-handling) - [Manual Serialization](#manual-serialization) - [Instruction Definition](#instruction-definition) - [State Management](#state-management) - [Manual CPI Patterns](#manual-cpi-patterns) - [Build and Deploy Workflow](#build-and-deploy-workflow) - [Testing with Mollusk](#testing-with-mollusk) - [Verified Builds](#verified-builds) - [Program Management](#program-management) - [Common Native Patterns](#common-native-patterns) --- ## Project Setup ### Cargo.toml Configuration Basic program configuration: ```toml [package] name = "my_program" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib", "lib"] # cdylib for .so, lib for tests name = "my_program" [features] no-entrypoint = [] # Disable entrypoint for testing/CPI [dependencies] solana-program = "2.1.0" borsh = "1.5.1" borsh-derive = "1.5.1" [dev-dependencies] mollusk-svm = "0.3.0" solana-sdk = "2.1.0" [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ``` ### Dependency Versions **Production Dependencies:** - `solana-program = "2.1.0"` - Core program runtime APIs - `borsh = "1.5.1"` - Serialization framework - `borsh-derive = "1.5.1"` - Derive macros for Borsh **Development Dependencies:** - `mollusk-svm = "0.3.0"` - Fast testing framework - `solana-sdk = "2.1.0"` - Client-side SDK for tests - `mollusk-svm-bencher = "0.3.0"` - Compute unit benchmarking **Optional Helpers:** - `thiserror = "2.0"` - Error type definitions - `num-derive = "0.4"` - Derive numeric traits - `num-traits = "0.2"` - Numeric trait support - `spl-token = "6.0"` - Token program integration - `spl-associated-token-account = "5.0"` - ATA integration - `bytemuck = "1.20"` - Zero-copy type conversions ### Workspace Setup Pattern For multi-program projects: ```toml # Workspace Cargo.toml [workspace] members = [ "programs/program-one", "programs/program-two", ] resolver = "2" [workspace.dependencies] solana-program = "2.1.0" borsh = "1.5.1" # Program Cargo.toml [dependencies] solana-program = { workspace = true } borsh = { workspace = true } ``` ### Project Structure ``` my-program/ ├── Cargo.toml ├── src/ │ ├── lib.rs # Entrypoint and routing │ ├── instruction.rs # Instruction definitions │ ├── state.rs # Account state structs │ ├── processor.rs # Instruction handlers │ ├── error.rs # Custom errors │ └── utils.rs # Helper functions ├── tests/ │ └── test.rs # Mollusk tests └── target/ └── deploy/ ├── program.so # Built program binary └── program-keypair.json # Program keypair ``` --- ## Entrypoint Patterns ### Basic Entrypoint The `entrypoint!` macro sets up the program entry: ```rust use solana_program::{ account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, }; // Declare the entrypoint entrypoint!(process_instruction); // Process instruction function signature pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { // Route to handlers Ok(()) } ``` ### Conditional Entrypoint (for testing/CPI) Disable entrypoint when used as a dependency: ```rust #[cfg(not(feature = "no-entrypoint"))] use solana_program::entrypoint; #[cfg(not(feature = "no-entrypoint"))] entrypoint!(process_instruction); pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { // Implementation Ok(()) } ``` ### Instruction Routing Pattern Route to different handlers based on instruction type: ```rust use borsh::BorshDeserialize; pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { // Deserialize instruction let instruction = MyInstruction::try_from_slice(instruction_data)?; // Route to handler match instruction { MyInstruction::Initialize { data } => { process_initialize(program_id, accounts, data) } MyInstruction::Update { new_data } => { process_update(program_id, accounts, new_data) } MyInstruction::Close => { process_close(program_id, accounts) } } } ``` ### Multi-Module Routing For larger programs, organize handlers in modules: ```rust mod processor; pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { let instruction = MyInstruction::try_from_slice(instruction_data)?; match instruction { MyInstruction::Initialize { data } => { processor::initialize::process(program_id, accounts, data) } MyInstruction::Update { new_data } => { processor::update::process(program_id, accounts, new_data) } MyInstruction::Close => { processor::close::process(program_id, accounts) } } } ``` --- ## Manual Account Handling ### Using next_account_info Iterator The standard pattern for accessing accounts: ```rust use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, }; fn process_transfer(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); // Get accounts in order let payer = next_account_info(account_info_iter)?; let recipient = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Use accounts... Ok(()) } ``` ### AccountInfo Structure and Methods Key fields and methods: ```rust pub struct AccountInfo<'a> { pub key: &'a Pubkey, // Account public key pub is_signer: bool, // Signed transaction? pub is_writable: bool, // Writable account? pub lamports: Rc>, // Account balance pub data: Rc>, // Account data pub owner: &'a Pubkey, // Owner program pub executable: bool, // Is executable? pub rent_epoch: Epoch, // Rent epoch } // Common methods impl<'a> AccountInfo<'a> { // Check if account signed the transaction pub fn is_signer(&self) -> bool; // Check if account is writable pub fn is_writable(&self) -> bool; // Borrow account data immutably pub fn data(&self) -> Ref<&mut [u8]>; // Borrow account data mutably pub fn data_mut(&self) -> RefMut<&mut [u8]>; // Borrow lamports immutably pub fn lamports(&self) -> Ref<&mut u64>; // Borrow lamports mutably pub fn lamports_mut(&self) -> RefMut<&mut u64>; // Get data length pub fn data_len(&self) -> usize; // Check if owned by program pub fn is_owned_by(&self, program_id: &Pubkey) -> bool; // Deserialize account data pub fn deserialize_data(&self) -> Result; // Serialize data into account pub fn serialize_data(&self, state: &T) -> Result<(), Error>; } ``` ### Explicit Account Validation Patterns **Signer Check:** ```rust if !account.is_signer { return Err(ProgramError::MissingRequiredSignature); } ``` **Writable Check:** ```rust if !account.is_writable { return Err(ProgramError::InvalidAccountData); } ``` **Owner Check:** ```rust if account.owner != program_id { return Err(ProgramError::IncorrectProgramId); } ``` **Specific Owner Check:** ```rust use solana_program::system_program; if account.owner != &system_program::ID { return Err(ProgramError::InvalidAccountOwner); } ``` **Combined Validation:** ```rust fn validate_account( account: &AccountInfo, expected_owner: &Pubkey, must_sign: bool, must_write: bool, ) -> ProgramResult { if must_sign && !account.is_signer { return Err(ProgramError::MissingRequiredSignature); } if must_write && !account.is_writable { return Err(ProgramError::InvalidAccountData); } if account.owner != expected_owner { return Err(ProgramError::IncorrectProgramId); } Ok(()) } ``` **PDA Validation:** ```rust fn validate_pda( account: &AccountInfo, seeds: &[&[u8]], program_id: &Pubkey, ) -> ProgramResult { let (expected_key, _bump) = Pubkey::find_program_address(seeds, program_id); if account.key != &expected_key { return Err(ProgramError::InvalidSeeds); } Ok(()) } ``` **Rent Exemption Check:** ```rust use solana_program::sysvar::{rent::Rent, Sysvar}; fn check_rent_exempt(account: &AccountInfo) -> ProgramResult { let rent = Rent::get()?; if !rent.is_exempt(account.lamports(), account.data_len()) { return Err(ProgramError::AccountNotRentExempt); } Ok(()) } ``` ### Account Data Access Patterns **Immutable Borrow:** ```rust let data = account.data.borrow(); let state = MyState::try_from_slice(&data)?; ``` **Mutable Borrow:** ```rust let mut data = account.data.borrow_mut(); let mut state = MyState::try_from_slice(&data)?; state.counter += 1; state.serialize(&mut &mut data[..])?; ``` **Lamport Access:** ```rust // Read lamports let balance = account.lamports(); println!("Balance: {}", *balance); // Modify lamports (for transfers) **account.lamports.borrow_mut() = new_balance; ``` **Zero-Copy Data Access:** ```rust use bytemuck::{Pod, Zeroable}; #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct FastState { value: u64, flag: u8, } fn read_fast_state(account: &AccountInfo) -> Result<&FastState, ProgramError> { let data = account.try_borrow_data()?; bytemuck::try_from_bytes(&data[..std::mem::size_of::()]) .map_err(|_| ProgramError::InvalidAccountData) } ``` --- ## Manual Serialization ### Borsh Derive Use `BorshSerialize` and `BorshDeserialize` for most cases: ```rust use borsh::{BorshSerialize, BorshDeserialize}; #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct MyState { pub is_initialized: bool, pub counter: u64, pub authority: Pubkey, pub data: Vec, } ``` ### Manual Borsh Implementation For custom serialization logic: ```rust use borsh::io::{Read, Write, Result as BorshResult}; #[derive(Debug)] pub struct CustomState { pub flag: bool, pub value: u64, } impl BorshSerialize for CustomState { fn serialize(&self, writer: &mut W) -> BorshResult<()> { self.flag.serialize(writer)?; self.value.serialize(writer)?; Ok(()) } } impl BorshDeserialize for CustomState { fn deserialize_reader(reader: &mut R) -> BorshResult { let flag = bool::deserialize_reader(reader)?; let value = u64::deserialize_reader(reader)?; Ok(Self { flag, value }) } } ``` ### Account Data Layout Planning Calculate and document exact byte offsets: ```rust // Account layout documentation // [0] is_initialized: bool (1 byte) // [1-8] counter: u64 (8 bytes) // [9-40] authority: Pubkey (32 bytes) // Total: 41 bytes #[derive(BorshSerialize, BorshDeserialize)] pub struct Counter { pub is_initialized: bool, // 1 byte pub counter: u64, // 8 bytes pub authority: Pubkey, // 32 bytes } impl Counter { pub const LEN: usize = 1 + 8 + 32; // 41 bytes } ``` ### Packing and Unpacking Account Data **Deserialize (unpack):** ```rust use borsh::BorshDeserialize; fn get_state(account: &AccountInfo) -> Result { let data = account.try_borrow_data()?; MyState::try_from_slice(&data) .map_err(|_| ProgramError::InvalidAccountData) } ``` **Serialize (pack):** ```rust use borsh::BorshSerialize; fn save_state(account: &AccountInfo, state: &MyState) -> ProgramResult { let mut data = account.try_borrow_mut_data()?; state.serialize(&mut &mut data[..]) .map_err(|_| ProgramError::InvalidAccountData)?; Ok(()) } ``` **Combined Pattern:** ```rust fn update_counter(account: &AccountInfo, increment: u64) -> ProgramResult { // Deserialize let mut data = account.try_borrow_mut_data()?; let mut state = MyState::try_from_slice(&data)?; // Modify state.counter += increment; // Serialize back state.serialize(&mut &mut data[..])?; Ok(()) } ``` ### Zero-Copy Patterns with Bytemuck For high-performance, use zero-copy with bytemuck: ```rust use bytemuck::{Pod, Zeroable, from_bytes_mut, bytes_of}; #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] pub struct ZeroCopyState { pub is_initialized: u8, // bool as u8 pub counter: u64, pub authority: [u8; 32], // Pubkey as bytes } impl ZeroCopyState { pub const LEN: usize = std::mem::size_of::(); } // Read zero-copy fn get_state(account: &AccountInfo) -> Result<&ZeroCopyState, ProgramError> { let data = account.try_borrow_data()?; bytemuck::try_from_bytes(&data[..ZeroCopyState::LEN]) .map_err(|_| ProgramError::InvalidAccountData) } // Write zero-copy fn update_state(account: &AccountInfo, new_counter: u64) -> ProgramResult { let mut data = account.try_borrow_mut_data()?; let state = bytemuck::try_from_bytes_mut::( &mut data[..ZeroCopyState::LEN] ).map_err(|_| ProgramError::InvalidAccountData)?; state.counter = new_counter; Ok(()) } ``` ### Variable-Length Data For dynamic data, use a header + data pattern: ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct VarLenState { pub is_initialized: bool, pub data_len: u32, // Followed by data_len bytes } impl VarLenState { pub const HEADER_LEN: usize = 1 + 4; // bool + u32 pub fn unpack(data: &[u8]) -> Result<(Self, &[u8]), ProgramError> { if data.len() < Self::HEADER_LEN { return Err(ProgramError::InvalidAccountData); } let header = Self::try_from_slice(&data[..Self::HEADER_LEN])?; let data_slice = &data[Self::HEADER_LEN..Self::HEADER_LEN + header.data_len as usize]; Ok((header, data_slice)) } } ``` --- ## Instruction Definition ### Borsh-Serializable Instruction Enums Define instructions as enums: ```rust use borsh::{BorshSerialize, BorshDeserialize}; use solana_program::pubkey::Pubkey; #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] pub enum MyInstruction { /// Initialize a new account /// /// Accounts expected: /// 0. `[writable, signer]` Account to initialize /// 1. `[signer]` Authority /// 2. `[]` System Program Initialize { initial_value: u64, }, /// Update account data /// /// Accounts expected: /// 0. `[writable]` Account to update /// 1. `[signer]` Authority Update { new_value: u64, }, /// Transfer ownership /// /// Accounts expected: /// 0. `[writable]` Account /// 1. `[signer]` Current authority /// 2. `[]` New authority TransferOwnership { new_authority: Pubkey, }, /// Close account and reclaim rent /// /// Accounts expected: /// 0. `[writable]` Account to close /// 1. `[writable]` Rent recipient /// 2. `[signer]` Authority Close, } ``` ### Instruction Data Layout **Fixed-Size Instructions:** ```rust // Discriminator (1 byte) + data // [0] = 0 -> Initialize // [1] = 1 -> Update // etc. #[derive(BorshSerialize, BorshDeserialize)] pub enum SimpleInstruction { Initialize = 0, Update = 1, Close = 2, } ``` **Instructions with Parameters:** ```rust // Manual discriminator pattern pub enum MyInstruction { // Discriminator 0: [0, value_bytes[0..8]] Initialize { value: u64 }, // Discriminator 1: [1, amount_bytes[0..8]] Transfer { amount: u64 }, } impl MyInstruction { pub fn unpack(input: &[u8]) -> Result { let (&discriminator, rest) = input.split_first() .ok_or(ProgramError::InvalidInstructionData)?; Ok(match discriminator { 0 => { let value = u64::from_le_bytes(rest[..8].try_into().unwrap()); Self::Initialize { value } } 1 => { let amount = u64::from_le_bytes(rest[..8].try_into().unwrap()); Self::Transfer { amount } } _ => return Err(ProgramError::InvalidInstructionData), }) } } ``` ### Dispatching Instructions **Pattern 1: Direct Match** ```rust pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { let instruction = MyInstruction::try_from_slice(instruction_data)?; match instruction { MyInstruction::Initialize { initial_value } => { msg!("Instruction: Initialize"); process_initialize(program_id, accounts, initial_value) } MyInstruction::Update { new_value } => { msg!("Instruction: Update"); process_update(program_id, accounts, new_value) } MyInstruction::Close => { msg!("Instruction: Close"); process_close(program_id, accounts) } } } ``` **Pattern 2: Handler Functions** ```rust impl MyInstruction { pub fn process( &self, program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { match self { Self::Initialize { initial_value } => { Self::process_initialize(program_id, accounts, *initial_value) } Self::Update { new_value } => { Self::process_update(program_id, accounts, *new_value) } Self::Close => { Self::process_close(program_id, accounts) } } } fn process_initialize( program_id: &Pubkey, accounts: &[AccountInfo], initial_value: u64, ) -> ProgramResult { // Implementation Ok(()) } } ``` --- ## State Management ### Defining Account State Structs ```rust use borsh::{BorshSerialize, BorshDeserialize}; use solana_program::pubkey::Pubkey; #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct UserAccount { pub is_initialized: bool, pub authority: Pubkey, pub balance: u64, pub last_updated: i64, } impl UserAccount { pub const LEN: usize = 1 + 32 + 8 + 8; // 49 bytes } ``` ### Calculating Account Sizes **Fixed-Size Accounts:** ```rust impl MyState { // Method 1: Manual calculation pub const LEN: usize = 1 + // is_initialized: bool 32 + // authority: Pubkey 8 + // counter: u64 4 + // data_len: u32 100; // data: [u8; 100] // Method 2: Use size_of pub const LEN_ALT: usize = std::mem::size_of::(); } ``` **Variable-Size Accounts:** ```rust impl DynamicState { pub const BASE_LEN: usize = 1 + 32 + 8; // Fixed fields pub fn calculate_size(data_len: usize) -> usize { Self::BASE_LEN + 4 + data_len // +4 for length prefix } } ``` **With Borsh:** ```rust use borsh::BorshSerialize; let state = MyState { /* ... */ }; let serialized = state.try_to_vec()?; let size = serialized.len(); // Actual size needed ``` ### Initializing Accounts Manually with System Program CPI **Complete Initialization Pattern:** ```rust use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, program::invoke, pubkey::Pubkey, rent::Rent, system_instruction, sysvar::Sysvar, }; fn process_initialize( program_id: &Pubkey, accounts: &[AccountInfo], initial_value: u64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let new_account = next_account_info(account_info_iter)?; let payer = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Calculate space needed let space = MyState::LEN; // Calculate rent let rent = Rent::get()?; let rent_lamports = rent.minimum_balance(space); // Create account via CPI to System Program invoke( &system_instruction::create_account( payer.key, // Funding account new_account.key, // New account rent_lamports, // Lamports space as u64, // Space program_id, // Owner ), &[ payer.clone(), new_account.clone(), system_program.clone(), ], )?; // Initialize account data let mut data = new_account.try_borrow_mut_data()?; let state = MyState { is_initialized: true, counter: initial_value, authority: *payer.key, }; state.serialize(&mut &mut data[..])?; Ok(()) } ``` **Initialize PDA Pattern:** ```rust use solana_program::program::invoke_signed; fn initialize_pda( program_id: &Pubkey, accounts: &[AccountInfo], seeds: &[&[u8]], bump: u8, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let pda = next_account_info(account_info_iter)?; let payer = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Verify PDA let (expected_pda, expected_bump) = Pubkey::find_program_address(seeds, program_id); if pda.key != &expected_pda || bump != expected_bump { return Err(ProgramError::InvalidSeeds); } // Create PDA account let space = MyState::LEN; let rent = Rent::get()?; let lamports = rent.minimum_balance(space); let bump_seed = &[bump]; let seeds_with_bump = &[seeds, &[bump_seed.as_slice()]].concat(); invoke_signed( &system_instruction::create_account( payer.key, pda.key, lamports, space as u64, program_id, ), &[payer.clone(), pda.clone(), system_program.clone()], &[seeds_with_bump], // Signer seeds )?; // Initialize data let mut data = pda.try_borrow_mut_data()?; let state = MyState::default(); state.serialize(&mut &mut data[..])?; Ok(()) } ``` ### Account Reallocation Resize account data: ```rust use solana_program::program::invoke; fn reallocate_account( account: &AccountInfo, payer: &AccountInfo, new_size: usize, program_id: &Pubkey, ) -> ProgramResult { // Verify ownership if account.owner != program_id { return Err(ProgramError::IncorrectProgramId); } // Reallocate account.realloc(new_size, false)?; // Fund additional rent if needed let rent = Rent::get()?; let new_minimum_balance = rent.minimum_balance(new_size); let current_balance = account.lamports(); if *current_balance < new_minimum_balance { let additional = new_minimum_balance - *current_balance; **payer.lamports.borrow_mut() -= additional; **account.lamports.borrow_mut() += additional; } Ok(()) } ``` --- ## Manual CPI Patterns ### Using invoke For CPIs without PDA signers: ```rust use solana_program::{ account_info::AccountInfo, instruction::{AccountMeta, Instruction}, program::invoke, pubkey::Pubkey, system_instruction, }; fn transfer_sol( from: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, amount: u64, ) -> ProgramResult { invoke( &system_instruction::transfer(from.key, to.key, amount), &[from.clone(), to.clone(), system_program.clone()], ) } ``` ### Using invoke_signed For CPIs with PDA signers: ```rust use solana_program::program::invoke_signed; fn pda_transfer( pda: &AccountInfo, recipient: &AccountInfo, system_program: &AccountInfo, amount: u64, seeds: &[&[u8]], bump: u8, ) -> ProgramResult { let bump_seed = &[bump]; let signer_seeds: &[&[&[u8]]] = &[ &[seeds, &[bump_seed]].concat() ]; invoke_signed( &system_instruction::transfer(pda.key, recipient.key, amount), &[pda.clone(), recipient.clone(), system_program.clone()], signer_seeds, ) } ``` ### Building AccountMeta Arrays Manually construct account metadata: ```rust use solana_program::instruction::AccountMeta; let account_metas = vec![ AccountMeta::new(*writable_account.key, false), // Writable, not signer AccountMeta::new(*writable_signer.key, true), // Writable, signer AccountMeta::new_readonly(*readonly_account.key, false), // Read-only, not signer AccountMeta::new_readonly(*readonly_signer.key, true), // Read-only, signer ]; ``` ### Creating Instruction Structs Build instructions for CPI: ```rust use solana_program::instruction::Instruction; fn build_custom_instruction( program_id: &Pubkey, account1: &Pubkey, account2: &Pubkey, data: Vec, ) -> Instruction { Instruction { program_id: *program_id, accounts: vec![ AccountMeta::new(*account1, true), AccountMeta::new(*account2, false), ], data, } } // Use in CPI fn call_custom_program( program: &AccountInfo, account1: &AccountInfo, account2: &AccountInfo, data: Vec, ) -> ProgramResult { let instruction = build_custom_instruction( program.key, account1.key, account2.key, data, ); invoke( &instruction, &[account1.clone(), account2.clone()], ) } ``` ### SPL Token CPI Pattern Transfer tokens via CPI: ```rust use spl_token::instruction as token_instruction; fn transfer_tokens( token_program: &AccountInfo, source: &AccountInfo, destination: &AccountInfo, authority: &AccountInfo, amount: u64, ) -> ProgramResult { invoke( &token_instruction::transfer( token_program.key, source.key, destination.key, authority.key, &[], // No multisig signers amount, )?, &[source.clone(), destination.clone(), authority.clone()], ) } fn transfer_tokens_with_pda( token_program: &AccountInfo, source: &AccountInfo, destination: &AccountInfo, pda_authority: &AccountInfo, amount: u64, seeds: &[&[u8]], bump: u8, ) -> ProgramResult { let bump_seed = &[bump]; let signer_seeds: &[&[&[u8]]] = &[ &[seeds, &[bump_seed]].concat() ]; invoke_signed( &token_instruction::transfer( token_program.key, source.key, destination.key, pda_authority.key, &[], amount, )?, &[source.clone(), destination.clone(), pda_authority.clone()], signer_seeds, ) } ``` --- ## Build and Deploy Workflow ### cargo build-sbf Command Build the program for Solana: ```bash # Basic build cargo build-sbf # Build with specific Solana version cargo build-sbf --solana-version 2.1.0 # Build for mainnet (with optimizations) cargo build-sbf --release # Specify output directory cargo build-sbf --sbf-out-dir ./output ``` ### Understanding .so and -keypair.json Files After building: ``` target/deploy/ ├── my_program.so # Compiled program binary └── my_program-keypair.json # Program's keypair (address) ``` **Program ID:** ```bash # Get program ID from keypair solana address -k target/deploy/my_program-keypair.json ``` **Update Program ID in Code:** ```rust // In lib.rs declare_id!("YourProgramID11111111111111111111111111111"); ``` ### solana program deploy Commands **Deploy to Devnet:** ```bash # Set cluster solana config set --url devnet # Fund deployer account solana airdrop 2 # Deploy program solana program deploy target/deploy/my_program.so # Deploy to specific program ID solana program deploy \ target/deploy/my_program.so \ --program-id target/deploy/my_program-keypair.json # Deploy with custom keypair solana program deploy \ target/deploy/my_program.so \ --program-id custom-keypair.json \ --upgrade-authority ~/.config/solana/id.json ``` **Deploy to Mainnet:** ```bash solana config set --url mainnet-beta # Deploy (costs SOL based on program size) solana program deploy target/deploy/my_program.so ``` ### Program Size and Cost Calculation **Check Program Size:** ```bash ls -lh target/deploy/my_program.so # Or get detailed info solana program show ``` **Calculate Deployment Cost:** Program cost formula: `rent_exemption(program_size)` ```bash # Get rent for specific size solana rent # Example for 200KB program solana rent 204800 # Output: Rent-exempt minimum: 1.42607328 SOL ``` **Typical Sizes:** - Simple programs: 50-100 KB - Medium programs: 100-300 KB - Large programs: 300-500 KB - Maximum: ~1 MB (hard limit) **Reduce Program Size:** ```toml # In Cargo.toml [profile.release] opt-level = "z" # Optimize for size lto = true # Link-time optimization codegen-units = 1 # Better optimization strip = true # Strip symbols ``` --- ## Testing with Mollusk ### Test Structure with mollusk-svm Basic test setup: ```rust #[cfg(test)] mod tests { use { mollusk_svm::Mollusk, solana_sdk::{ account::Account, instruction::{AccountMeta, Instruction}, pubkey::Pubkey, }, }; #[test] fn test_initialize() { // Create Mollusk instance let program_id = Pubkey::new_unique(); let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); // Test implementation... } } ``` ### Creating Test Accounts **System-Owned Account:** ```rust let user = Pubkey::new_unique(); let user_account = Account { lamports: 1_000_000, data: vec![], owner: solana_sdk::system_program::id(), executable: false, rent_epoch: 0, }; ``` **Program-Owned Account:** ```rust let state_account = Pubkey::new_unique(); let state = Account { lamports: rent_lamports, data: vec![0; MyState::LEN], owner: program_id, executable: false, rent_epoch: 0, }; ``` **Pre-Initialized Account:** ```rust use borsh::BorshSerialize; let mut data = vec![0; MyState::LEN]; let initial_state = MyState { is_initialized: true, counter: 42, authority: user, }; initial_state.serialize(&mut data.as_mut_slice()).unwrap(); let initialized_account = Account { lamports: rent_lamports, data, owner: program_id, executable: false, rent_epoch: 0, }; ``` ### Process Instructions and Validate Results **Basic Process and Check:** ```rust use mollusk_svm::result::Check; #[test] fn test_instruction() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let user = Pubkey::new_unique(); let instruction = Instruction::new_with_bytes( program_id, &[0], // Instruction data vec![ AccountMeta::new(user, true), ], ); let accounts = vec![ (user, Account { lamports: 1_000_000, data: vec![], owner: solana_sdk::system_program::id(), executable: false, rent_epoch: 0, }), ]; let checks = vec![ Check::success(), Check::account(&user) .lamports(1_000_000) .build(), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } ``` **Validate Account Data:** ```rust let expected_data = MyState { is_initialized: true, counter: 10, authority: user, }.try_to_vec().unwrap(); let checks = vec![ Check::success(), Check::account(&state_account) .data(&expected_data) .lamports(rent_lamports) .owner(&program_id) .build(), ]; ``` **Check Specific Data Slice:** ```rust let checks = vec![ Check::success(), Check::account(&account) .data_slice(0, &[1]) // Check first byte is 1 (initialized) .data_slice(8, &10u64.to_le_bytes()) // Check counter at offset 8 .build(), ]; ``` **Test Error Conditions:** ```rust use solana_sdk::instruction::InstructionError; let checks = vec![ Check::instruction_err(InstructionError::InvalidInstructionData), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); ``` ### Compute Unit Benchmarking **Basic Benchmark:** ```rust use mollusk_svm_bencher::MolluskComputeUnitBencher; fn main() { let program_id = Pubkey::new_unique(); let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let instruction = /* build instruction */; let accounts = /* setup accounts */; MolluskComputeUnitBencher::new(mollusk) .bench(("my_instruction", &instruction, &accounts)) .must_pass(true) .out_dir("./benches") .execute(); } ``` **Run Benchmark:** ```bash # Build first cargo build-sbf # Run benchmark cargo run --bin bench ``` **Benchmark Output:** ``` ╭──────────────────────────────┬────────────────────╮ │ Instruction │ Compute Units │ ├──────────────────────────────┼────────────────────┤ │ my_instruction │ 1,234 │ ╰──────────────────────────────┴────────────────────╯ Results written to: ./benches/compute_units.json ``` --- ## Verified Builds ### solana-verify Workflow Verify programs on-chain match source code: **Install solana-verify:** ```bash cargo install solana-verify ``` **Verify a Program:** ```bash # Verify remote build solana-verify verify-from-repo \ --program-id \ --remote https://github.com/user/repo \ --commit-hash \ --library-name program_name # Verify with mount path (for workspace) solana-verify verify-from-repo \ --program-id \ --remote https://github.com/user/repo \ --commit-hash \ --mount-path programs/my-program \ --library-name my_program ``` ### Docker-Based Builds Build in Docker for reproducibility: **Dockerfile:** ```dockerfile FROM --platform=linux/amd64 projectserum/build:v0.29.0 WORKDIR /build COPY . . RUN cargo build-sbf --release ``` **Build Command:** ```bash docker build -t my-program-build . docker create --name extract my-program-build docker cp extract:/build/target/deploy/my_program.so ./my_program-verifiable.so docker rm extract ``` **Verify Deterministic:** ```bash # Compare hashes sha256sum target/deploy/my_program.so sha256sum my_program-verifiable.so # Should match! ``` ### Buffer Uploads for Multisig Deploy via buffer for multisig upgrade authority: ```bash # Write program to buffer solana program write-buffer target/deploy/my_program.so # Output: Buffer: # Set buffer authority to multisig solana program set-buffer-authority --new-buffer-authority # Later: Deploy from buffer (requires multisig) solana program deploy --buffer --program-id ``` **Squads Multisig Example:** ```bash # 1. Write buffer BUFFER=$(solana program write-buffer target/deploy/my_program.so | grep "Buffer:" | awk '{print $2}') # 2. Transfer buffer authority to Squads solana program set-buffer-authority $BUFFER --new-buffer-authority # 3. Create proposal in Squads UI to deploy from buffer ``` --- ## Program Management ### solana program show Get program information: ```bash # Show program details solana program show # Output: # Program Id: # Owner: BPFLoaderUpgradeab1e11111111111111111111111 # ProgramData Address: # Authority: # Last Deployed In Slot: 123456789 # Data Length: 204800 bytes # Balance: 1.42607328 SOL ``` **Show Program Data:** ```bash # Get upgrade authority solana program show | grep Authority # Get program size solana program show | grep "Data Length" ``` ### Authority Transfers **Transfer Upgrade Authority:** ```bash # Transfer to new authority solana program set-upgrade-authority \ \ --new-upgrade-authority # Transfer to multisig solana program set-upgrade-authority \ \ --new-upgrade-authority ``` ### Making Programs Immutable Remove upgrade authority to make program immutable: ```bash # Make immutable (IRREVERSIBLE!) solana program set-upgrade-authority --final # Verify immutability solana program show # Authority: none ``` **Warning:** This is permanent. The program can never be upgraded again. ### Closing Programs Reclaim rent from closed programs: ```bash # Close program and reclaim rent solana program close # Close and send rent to specific recipient solana program close --recipient # Close program buffer solana program close --buffers ``` **Requirements:** - Must be upgrade authority - Program must not be marked as final - Recipient receives all lamports from program account --- ## Common Native Patterns ### PDA Derivation and Signing **Find PDA:** ```rust use solana_program::pubkey::Pubkey; fn get_user_pda(user: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address( &[ b"user", user.as_ref(), ], program_id, ) } ``` **Verify PDA:** ```rust fn validate_pda( pda: &AccountInfo, seeds: &[&[u8]], bump: u8, program_id: &Pubkey, ) -> ProgramResult { let expected_pda = Pubkey::create_program_address( &[seeds, &[&[bump]]].concat(), program_id, )?; if pda.key != &expected_pda { return Err(ProgramError::InvalidSeeds); } Ok(()) } ``` **Sign with PDA:** ```rust use solana_program::program::invoke_signed; fn pda_invoke( instruction: &Instruction, accounts: &[AccountInfo], user: &Pubkey, bump: u8, ) -> ProgramResult { let signer_seeds: &[&[&[u8]]] = &[ &[b"user", user.as_ref(), &[bump]] ]; invoke_signed(instruction, accounts, signer_seeds) } ``` ### Rent Calculation **Calculate Minimum Balance:** ```rust use solana_program::{ rent::Rent, sysvar::Sysvar, }; fn get_rent_exempt_balance(data_len: usize) -> Result { let rent = Rent::get()?; Ok(rent.minimum_balance(data_len)) } ``` **Check if Rent Exempt:** ```rust fn is_rent_exempt(account: &AccountInfo) -> Result { let rent = Rent::get()?; Ok(rent.is_exempt(account.lamports(), account.data_len())) } ``` ### Lamport Transfers **Direct Transfer (modify lamports):** ```rust fn transfer_lamports( from: &AccountInfo, to: &AccountInfo, amount: u64, ) -> ProgramResult { // Borrow and update lamports **from.try_borrow_mut_lamports()? -= amount; **to.try_borrow_mut_lamports()? += amount; Ok(()) } ``` **Via System Program:** ```rust use solana_program::{ program::invoke, system_instruction, }; fn transfer_via_system_program( from: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, amount: u64, ) -> ProgramResult { invoke( &system_instruction::transfer(from.key, to.key, amount), &[from.clone(), to.clone(), system_program.clone()], ) } ``` ### Error Handling with ProgramError **Using Built-in Errors:** ```rust use solana_program::program_error::ProgramError; if !account.is_signer { return Err(ProgramError::MissingRequiredSignature); } if account.owner != program_id { return Err(ProgramError::IncorrectProgramId); } if account.data_len() < MyState::LEN { return Err(ProgramError::AccountDataTooSmall); } ``` **Custom Errors:** ```rust use solana_program::program_error::ProgramError; use thiserror::Error; #[derive(Error, Debug, Copy, Clone)] pub enum MyError { #[error("Account already initialized")] AlreadyInitialized, #[error("Invalid authority")] InvalidAuthority, #[error("Arithmetic overflow")] Overflow, } impl From for ProgramError { fn from(e: MyError) -> Self { ProgramError::Custom(e as u32) } } // Usage if state.is_initialized { return Err(MyError::AlreadyInitialized.into()); } ``` **With num_derive:** ```rust use num_derive::FromPrimitive; use num_traits::FromPrimitive; use solana_program::{ decode_error::DecodeError, program_error::{PrintProgramError, ProgramError}, }; use thiserror::Error; #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] pub enum MyError { #[error("Already initialized")] AlreadyInitialized, #[error("Invalid authority")] InvalidAuthority, } impl From for ProgramError { fn from(e: MyError) -> Self { ProgramError::Custom(e as u32) } } impl DecodeError for MyError { fn type_of() -> &'static str { "MyError" } } impl PrintProgramError for MyError { fn print(&self) where E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, { match self { MyError::AlreadyInitialized => msg!("Error: Already initialized"), MyError::InvalidAuthority => msg!("Error: Invalid authority"), } } } ``` ### Logging and Debugging **Basic Logging:** ```rust use solana_program::msg; msg!("Processing instruction"); msg!("Counter value: {}", counter); msg!("Account: {}, balance: {}", account.key, account.lamports()); ``` **Compute Units Logging:** ```rust use solana_program::log::sol_log_compute_units; sol_log_compute_units(); // Log current compute units used ``` **Data Logging:** ```rust use solana_program::log::sol_log_data; // Log data for off-chain processing sol_log_data(&[b"event", &event_data]); ``` ### Clock Access Get current timestamp and slot: ```rust use solana_program::{ clock::Clock, sysvar::Sysvar, }; fn get_current_time() -> Result { let clock = Clock::get()?; Ok(clock.unix_timestamp) } fn get_current_slot() -> Result { let clock = Clock::get()?; Ok(clock.slot) } ``` ### Account Closure Pattern Properly close accounts and reclaim rent: ```rust fn close_account( account_to_close: &AccountInfo, destination: &AccountInfo, ) -> ProgramResult { // Transfer all lamports let dest_starting_lamports = destination.lamports(); **destination.lamports.borrow_mut() = dest_starting_lamports .checked_add(account_to_close.lamports()) .ok_or(ProgramError::ArithmeticOverflow)?; **account_to_close.lamports.borrow_mut() = 0; // Zero out data let mut data = account_to_close.try_borrow_mut_data()?; data.fill(0); Ok(()) } ``` ### Discriminator Pattern Add discriminator to distinguish account types: ```rust #[derive(BorshSerialize, BorshDeserialize)] pub enum AccountType { Uninitialized, User, Config, } #[derive(BorshSerialize, BorshDeserialize)] pub struct UserAccount { pub account_type: AccountType, // Discriminator pub authority: Pubkey, pub balance: u64, } impl UserAccount { pub const LEN: usize = 1 + 32 + 8; pub fn validate_type(account: &AccountInfo) -> ProgramResult { let data = account.try_borrow_data()?; let account_type = AccountType::try_from_slice(&data[..1])?; match account_type { AccountType::User => Ok(()), _ => Err(ProgramError::InvalidAccountData), } } } ``` --- ## Additional Resources - **Solana Program Examples**: https://github.com/solana-developers/program-examples - **Mollusk Testing**: https://github.com/anza-xyz/mollusk - **solana-program Docs**: https://docs.rs/solana-program - **Solana Cookbook**: https://solanacookbook.com/ - **SPL Token**: https://spl.solana.com/token - **Solana Verify**: https://github.com/Ellipsis-Labs/solana-verifiable-build --- *This reference focuses on native Rust implementation patterns. For conceptual understanding of Solana primitives (PDAs, CPIs, accounts, etc.), see the other reference files in this directory.*