Files
2025-11-30 09:01:25 +08:00

22 KiB
Raw Permalink Blame History

Built-in Programs

This reference provides comprehensive coverage of Solana's built-in programs for native Rust development, focusing on the System Program and Compute Budget Program.

Table of Contents

  1. Overview of Built-in Programs
  2. System Program
  3. Compute Budget Program
  4. Other Built-in Programs
  5. CPI Patterns
  6. Best Practices

Overview of Built-in Programs

Built-in programs (also called native programs) are fundamental Solana programs that provide core blockchain functionality.

Key Built-in Programs

Program Program ID Purpose
System Program 11111111111111111111111111111111 Account creation, transfers, allocation
Compute Budget ComputeBudget111111111111111111111111111111 CU limits, heap size, priority fees
BPF Loader Various Loading and executing programs
Config Program Config1111111111111111111111111111111111111 Validator configuration
Stake Program Stake11111111111111111111111111111111111111 Staking and delegation
Vote Program Vote111111111111111111111111111111111111111 Validator voting

This reference focuses on the two most commonly used in program development: System Program and Compute Budget Program.


System Program

Program ID: solana_program::system_program::ID (11111111111111111111111111111111)

The System Program is responsible for account creation, lamport transfers, and account management.

Core Functionality

  1. Create accounts (regular and PDAs)
  2. Transfer lamports between accounts
  3. Allocate space for account data
  4. Assign ownership to programs
  5. Create nonce accounts for durable transactions

System Program Instructions

use solana_program::system_instruction;

pub enum SystemInstruction {
    CreateAccount,        // Create new account
    Assign,               // Assign account to program
    Transfer,             // Transfer lamports
    CreateAccountWithSeed,// Create account with seed
    AdvanceNonceAccount,  // Advance nonce
    WithdrawNonceAccount, // Withdraw from nonce
    InitializeNonceAccount, // Initialize nonce
    Allocate,             // Allocate account space
    AllocateWithSeed,     // Allocate with seed
    AssignWithSeed,       // Assign with seed
    TransferWithSeed,     // Transfer with seed
    UpgradeNonceAccount,  // Upgrade nonce (v4)
}

CreateAccount

Creates a new account with lamports and data space.

Function Signature

pub fn create_account(
    from_pubkey: &Pubkey,      // Funding account (must be signer)
    to_pubkey: &Pubkey,        // New account address
    lamports: u64,             // Lamports to fund account
    space: u64,                // Bytes of data space
    owner: &Pubkey,            // Program that will own the account
) -> Instruction

Usage in Native Rust

use solana_program::{
    system_instruction,
    program::invoke,
};

pub fn create_new_account(
    payer: &AccountInfo,
    new_account: &AccountInfo,
    system_program: &AccountInfo,
    program_id: &Pubkey,
) -> ProgramResult {
    let space = 100;  // Account data size
    let rent = Rent::get()?;
    let lamports = rent.minimum_balance(space);

    let create_account_ix = system_instruction::create_account(
        payer.key,
        new_account.key,
        lamports,
        space as u64,
        program_id,
    );

    invoke(
        &create_account_ix,
        &[
            payer.clone(),
            new_account.clone(),
            system_program.clone(),
        ],
    )?;

    msg!("Created account with {} bytes", space);
    Ok(())
}

Creating PDA Accounts

use solana_program::program::invoke_signed;

pub fn create_pda_account(
    payer: &AccountInfo,
    pda_account: &AccountInfo,
    system_program: &AccountInfo,
    program_id: &Pubkey,
    seeds: &[&[u8]],
    bump: u8,
) -> ProgramResult {
    // Verify PDA
    let (expected_pda, _bump) = Pubkey::find_program_address(seeds, program_id);
    if expected_pda != *pda_account.key {
        return Err(ProgramError::InvalidSeeds);
    }

    let space = 200;
    let rent = Rent::get()?;
    let lamports = rent.minimum_balance(space);

    let create_account_ix = system_instruction::create_account(
        payer.key,
        pda_account.key,
        lamports,
        space as u64,
        program_id,
    );

    // Create full seeds with bump
    let mut full_seeds = seeds.to_vec();
    full_seeds.push(&[bump]);
    let signer_seeds: &[&[&[u8]]] = &[&full_seeds];

    invoke_signed(
        &create_account_ix,
        &[payer.clone(), pda_account.clone(), system_program.clone()],
        signer_seeds,
    )?;

    msg!("Created PDA account at {}", pda_account.key);
    Ok(())
}

Transfer

Transfers lamports from one account to another.

Function Signature

pub fn transfer(
    from_pubkey: &Pubkey,     // Source account (must be signer)
    to_pubkey: &Pubkey,       // Destination account
    lamports: u64,            // Amount to transfer
) -> Instruction

Usage in Native Rust

pub fn transfer_lamports(
    from: &AccountInfo,
    to: &AccountInfo,
    system_program: &AccountInfo,
    amount: u64,
) -> ProgramResult {
    let transfer_ix = system_instruction::transfer(
        from.key,
        to.key,
        amount,
    );

    invoke(
        &transfer_ix,
        &[from.clone(), to.clone(), system_program.clone()],
    )?;

    msg!("Transferred {} lamports from {} to {}",
        amount, from.key, to.key);
    Ok(())
}

Transfer from PDA

pub fn transfer_from_pda(
    pda: &AccountInfo,
    to: &AccountInfo,
    system_program: &AccountInfo,
    amount: u64,
    seeds: &[&[u8]],
    bump: u8,
) -> ProgramResult {
    let transfer_ix = system_instruction::transfer(
        pda.key,
        to.key,
        amount,
    );

    let mut full_seeds = seeds.to_vec();
    full_seeds.push(&[bump]);
    let signer_seeds: &[&[&[u8]]] = &[&full_seeds];

    invoke_signed(
        &transfer_ix,
        &[pda.clone(), to.clone(), system_program.clone()],
        signer_seeds,
    )?;

    Ok(())
}

Allocate

Allocates space for an account's data.

Function Signature

pub fn allocate(
    pubkey: &Pubkey,          // Account to allocate (must be signer)
    space: u64,               // Bytes to allocate
) -> Instruction

Usage in Native Rust

pub fn allocate_account_space(
    account: &AccountInfo,
    system_program: &AccountInfo,
    space: u64,
) -> ProgramResult {
    let allocate_ix = system_instruction::allocate(
        account.key,
        space,
    );

    invoke(
        &allocate_ix,
        &[account.clone(), system_program.clone()],
    )?;

    msg!("Allocated {} bytes for account", space);
    Ok(())
}

⚠️ Note: The account must be owned by the System Program before allocating. Most programs use create_account instead, which combines allocation with ownership assignment.


Assign

Assigns an account to a program (changes owner).

Function Signature

pub fn assign(
    pubkey: &Pubkey,          // Account to assign (must be signer)
    owner: &Pubkey,           // New owner program
) -> Instruction

Usage in Native Rust

pub fn assign_to_program(
    account: &AccountInfo,
    system_program: &AccountInfo,
    new_owner: &Pubkey,
) -> ProgramResult {
    let assign_ix = system_instruction::assign(
        account.key,
        new_owner,
    );

    invoke(
        &assign_ix,
        &[account.clone(), system_program.clone()],
    )?;

    msg!("Assigned account to program {}", new_owner);
    Ok(())
}

⚠️ Note: Most programs use create_account which handles assignment during creation.


Complete Example: Account Lifecycle

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    program::invoke_signed,
    pubkey::Pubkey,
    system_instruction,
    sysvar::{rent::Rent, Sysvar},
};

#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserData {
    pub user: Pubkey,
    pub balance: u64,
    pub created_at: i64,
}

pub fn create_user_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    user_pubkey: Pubkey,
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let payer = next_account_info(account_info_iter)?;
    let user_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // 1. Derive PDA
    let seeds = &[b"user", user_pubkey.as_ref()];
    let (pda, bump) = Pubkey::find_program_address(seeds, program_id);

    if pda != *user_account.key {
        return Err(ProgramError::InvalidSeeds);
    }

    // 2. Calculate space and rent
    let space = std::mem::size_of::<UserData>();
    let rent = Rent::get()?;
    let lamports = rent.minimum_balance(space);

    // 3. Create account via System Program CPI
    let create_ix = system_instruction::create_account(
        payer.key,
        user_account.key,
        lamports,
        space as u64,
        program_id,
    );

    let signer_seeds: &[&[&[u8]]] = &[&[b"user", user_pubkey.as_ref(), &[bump]]];

    invoke_signed(
        &create_ix,
        &[payer.clone(), user_account.clone(), system_program.clone()],
        signer_seeds,
    )?;

    // 4. Initialize account data
    let clock = Clock::get()?;
    let user_data = UserData {
        user: user_pubkey,
        balance: 0,
        created_at: clock.unix_timestamp,
    };

    user_data.serialize(&mut &mut user_account.data.borrow_mut()[..])?;

    msg!("Created user account for {}", user_pubkey);
    Ok(())
}

Compute Budget Program

Program ID: solana_program::compute_budget::ID (ComputeBudget111111111111111111111111111111)

The Compute Budget Program allows transactions to request specific compute unit limits, heap sizes, and priority fees.

Core Functionality

  1. Set compute unit limit - Maximum CUs for transaction
  2. Set compute unit price - Priority fee per CU
  3. Request heap size - Heap memory allocation

Compute Budget Instructions

use solana_program::compute_budget::ComputeBudgetInstruction;

pub enum ComputeBudgetInstruction {
    RequestUnitsDeprecated,      // Deprecated
    RequestHeapFrame(u32),       // Request heap frame (bytes)
    SetComputeUnitLimit(u32),    // Set max CUs
    SetComputeUnitPrice(u64),    // Set priority fee (microlamports per CU)
    SetLoadedAccountsDataSizeLimit(u32), // Set loaded accounts data limit
}

SetComputeUnitLimit

Sets the maximum compute units available to the transaction.

Function Signature

pub fn set_compute_unit_limit(units: u32) -> Instruction

Default Limits

  • Default per instruction: 200,000 CUs
  • Default per transaction: 1,400,000 CUs (with requested CU limit)
  • Maximum: 1,400,000 CUs

Usage in Native Rust

Important: Compute Budget instructions are added to the transaction by the client, not inside the program.

Client-side example (for reference):

// This code runs CLIENT-SIDE, not in the program
use solana_sdk::{
    compute_budget::ComputeBudgetInstruction,
    transaction::Transaction,
};

let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000);

let transaction = Transaction::new_signed_with_payer(
    &[
        compute_budget_ix,  // Must be first
        your_program_ix,
    ],
    Some(&payer.pubkey()),
    &[&payer],
    recent_blockhash,
);

⚠️ Note: Programs cannot modify their own compute budget. These instructions must be added client-side before sending the transaction.


SetComputeUnitPrice

Sets the priority fee per compute unit (for transaction prioritization).

Function Signature

pub fn set_compute_unit_price(microlamports: u64) -> Instruction

Priority Fee Calculation

Total Priority Fee = (CUs Used × microlamports) / 1,000,000

Example:

  • CUs used: 50,000
  • Price: 10,000 microlamports per CU
  • Fee: (50,000 × 10,000) / 1,000,000 = 500 lamports

Usage (Client-side)

// Client-side code
let compute_unit_price_ix = ComputeBudgetInstruction::set_compute_unit_price(20_000);

let transaction = Transaction::new_signed_with_payer(
    &[
        compute_unit_price_ix,  // Set priority fee
        your_program_ix,
    ],
    Some(&payer.pubkey()),
    &[&payer],
    recent_blockhash,
);

Use cases:

  • High-priority transactions (arbitrage, liquidations)
  • Congested network periods
  • Time-sensitive operations

RequestHeapFrame

Requests additional heap memory for the transaction.

Function Signature

pub fn request_heap_frame(bytes: u32) -> Instruction

Default Heap

  • Default: 32 KB
  • Maximum: 256 KB

Usage (Client-side)

// Client-side code
let heap_size_ix = ComputeBudgetInstruction::request_heap_frame(256 * 1024); // 256 KB

let transaction = Transaction::new_signed_with_payer(
    &[
        heap_size_ix,       // Request more heap
        your_program_ix,
    ],
    Some(&payer.pubkey()),
    &[&payer],
    recent_blockhash,
);

When to use:

  • Large data structures
  • Complex deserialization
  • Temporary buffers

⚠️ Cost: Requesting heap increases CU consumption.


SetLoadedAccountsDataSizeLimit

Sets the maximum total size of loaded account data.

Function Signature

pub fn set_loaded_accounts_data_size_limit(bytes: u32) -> Instruction

Default Limit

  • Default: 64 MB per transaction

Usage (Client-side)

// Client-side code
let accounts_data_limit_ix =
    ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(128 * 1024 * 1024);

let transaction = Transaction::new_signed_with_payer(
    &[
        accounts_data_limit_ix,
        your_program_ix,
    ],
    Some(&payer.pubkey()),
    &[&payer],
    recent_blockhash,
);

Use cases:

  • Transactions with many large accounts
  • Bulk processing operations

Complete Client-side Example

use solana_sdk::{
    compute_budget::ComputeBudgetInstruction,
    transaction::Transaction,
    signature::{Keypair, Signer},
    pubkey::Pubkey,
};

pub fn build_optimized_transaction(
    payer: &Keypair,
    program_id: &Pubkey,
    program_ix_data: &[u8],
    accounts: Vec<AccountMeta>,
    recent_blockhash: Hash,
) -> Transaction {
    // 1. Set compute unit limit (if default 200k is insufficient)
    let compute_limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000);

    // 2. Set priority fee (for faster processing)
    let compute_price_ix = ComputeBudgetInstruction::set_compute_unit_price(10_000);

    // 3. Request additional heap if needed
    let heap_size_ix = ComputeBudgetInstruction::request_heap_frame(128 * 1024); // 128 KB

    // 4. Your program instruction
    let program_ix = Instruction {
        program_id: *program_id,
        accounts,
        data: program_ix_data.to_vec(),
    };

    // 5. Build transaction (compute budget instructions FIRST)
    Transaction::new_signed_with_payer(
        &[
            compute_limit_ix,
            compute_price_ix,
            heap_size_ix,
            program_ix,
        ],
        Some(&payer.pubkey()),
        &[payer],
        recent_blockhash,
    )
}

Other Built-in Programs

BPF Loader

Purpose: Loads and executes Solana programs.

Program IDs:

  • BPFLoader1111111111111111111111111111111111 (deprecated)
  • BPFLoader2111111111111111111111111111111111 (upgradeable)
  • BPFLoaderUpgradeab1e11111111111111111111111 (current)

Usage: Primarily used by the runtime. Programs rarely interact with BPF Loader directly.

Stake Program

Program ID: Stake11111111111111111111111111111111111111

Purpose: Staking SOL to validators.

Common operations:

  • Create stake accounts
  • Delegate stake
  • Deactivate stake
  • Withdraw stake

Use case: Staking pools, liquid staking protocols.

Vote Program

Program ID: Vote111111111111111111111111111111111111111

Purpose: Validator voting and consensus.

Use case: Validator operations, rarely used by general programs.


CPI Patterns

System Program CPI Pattern

Standard pattern for calling System Program:

use solana_program::{
    program::invoke,
    system_instruction,
};

pub fn system_program_cpi(
    from: &AccountInfo,
    to: &AccountInfo,
    system_program: &AccountInfo,
) -> ProgramResult {
    // 1. Verify System Program
    if system_program.key != &solana_program::system_program::ID {
        return Err(ProgramError::IncorrectProgramId);
    }

    // 2. Create instruction
    let ix = system_instruction::transfer(from.key, to.key, 1_000_000);

    // 3. Invoke
    invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?;

    Ok(())
}

PDA Signing Pattern

When PDAs need to sign:

pub fn pda_system_cpi(
    pda: &AccountInfo,
    to: &AccountInfo,
    system_program: &AccountInfo,
    program_id: &Pubkey,
    seeds: &[&[u8]],
    bump: u8,
) -> ProgramResult {
    // 1. Verify PDA
    let (expected_pda, _) = Pubkey::find_program_address(seeds, program_id);
    if expected_pda != *pda.key {
        return Err(ProgramError::InvalidSeeds);
    }

    // 2. Create instruction
    let ix = system_instruction::transfer(pda.key, to.key, 500_000);

    // 3. Prepare signer seeds
    let mut full_seeds = seeds.to_vec();
    full_seeds.push(&[bump]);
    let signer_seeds: &[&[&[u8]]] = &[&full_seeds];

    // 4. Invoke with PDA signature
    invoke_signed(
        &ix,
        &[pda.clone(), to.clone(), system_program.clone()],
        signer_seeds,
    )?;

    Ok(())
}

Validation Pattern

Always validate accounts before CPI:

pub fn safe_system_cpi(
    from: &AccountInfo,
    to: &AccountInfo,
    system_program: &AccountInfo,
    amount: u64,
) -> ProgramResult {
    // ✅ Validate System Program
    if system_program.key != &solana_program::system_program::ID {
        msg!("Invalid System Program");
        return Err(ProgramError::IncorrectProgramId);
    }

    // ✅ Validate signer
    if !from.is_signer {
        msg!("From account must be signer");
        return Err(ProgramError::MissingRequiredSignature);
    }

    // ✅ Validate sufficient balance
    if from.lamports() < amount {
        msg!("Insufficient balance");
        return Err(ProgramError::InsufficientFunds);
    }

    // Execute CPI
    let ix = system_instruction::transfer(from.key, to.key, amount);
    invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?;

    Ok(())
}

Best Practices

1. Always Validate Program IDs

// ✅ Validate before CPI
if system_program.key != &solana_program::system_program::ID {
    return Err(ProgramError::IncorrectProgramId);
}

2. Use Rent Exemption

// ✅ Always create accounts with rent exemption
let rent = Rent::get()?;
let lamports = rent.minimum_balance(space);

// ❌ Don't use arbitrary amounts
let lamports = 1_000_000; // May not be rent-exempt!

3. Verify PDA Before Creation

// ✅ Verify PDA derivation
let (expected_pda, bump) = Pubkey::find_program_address(seeds, program_id);
if expected_pda != *pda_account.key {
    return Err(ProgramError::InvalidSeeds);
}

4. Use invoke_signed for PDAs

// ✅ PDAs sign with invoke_signed
invoke_signed(&ix, accounts, signer_seeds)?;

// ❌ Regular invoke won't work for PDA signers
invoke(&ix, accounts)?; // Fails if PDA needs to sign

5. Set Compute Budget Client-side

// ✅ Add compute budget instructions in client
let ixs = vec![
    ComputeBudgetInstruction::set_compute_unit_limit(400_000),
    your_program_ix,
];

// ❌ Cannot set from within program
// Programs cannot modify their own compute budget

6. Order Compute Budget Instructions First

// ✅ Compute budget instructions FIRST
let ixs = vec![
    compute_limit_ix,
    compute_price_ix,
    heap_size_ix,
    program_ix,
];

// ❌ Wrong order - may not apply
let ixs = vec![
    program_ix,
    compute_limit_ix,  // Too late!
];

7. Check Account Ownership Before Transfer

// ✅ Validate ownership for security
if from_account.owner != &solana_program::system_program::ID {
    msg!("Can only transfer from System-owned accounts");
    return Err(ProgramError::IllegalOwner);
}

Summary

Key Takeaways:

  1. System Program handles account creation, transfers, and allocation
  2. Compute Budget Program instructions are added client-side, not in programs
  3. Always validate program IDs before CPI
  4. Use rent exemption when creating accounts
  5. PDAs require invoke_signed for signing operations

Most Common Operations:

Operation Instruction Use Case
Create account create_account New program accounts
Transfer lamports transfer SOL transfers
Set CU limit set_compute_unit_limit High-CU transactions
Set priority fee set_compute_unit_price Fast transaction processing
Request heap request_heap_frame Large data operations

System Program CPI Template:

// Validate
if system_program.key != &solana_program::system_program::ID {
    return Err(ProgramError::IncorrectProgramId);
}

// Create instruction
let ix = system_instruction::transfer(from.key, to.key, amount);

// Invoke (or invoke_signed for PDAs)
invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?;

Compute Budget Client Template:

// Client-side
let ixs = vec![
    ComputeBudgetInstruction::set_compute_unit_limit(300_000),
    ComputeBudgetInstruction::set_compute_unit_price(10_000),
    your_program_ix,
];

Master these built-in programs for efficient account management and transaction optimization in production Solana programs.