Skip to content

BPF Loader Upgradeable programs not automatically loaded after deployment #240

@alex-sumner

Description

@alex-sumner

BPF Loader Upgradeable programs not automatically loaded after deployment

Description

When deploying programs using the BPF Loader Upgradeable, the deployed program accounts are not automatically available for execution in subsequent transactions. This occurs because:

  1. Program deployment creates both a Program account and ProgramData account
  2. After transaction execution, only the ProgramData account appears in the ExecutionRecord
  3. The Program account exists in the account database but lacks the executable flag
  4. The program is not loaded into the program cache
  5. Subsequent attempts to call the program fail with "program not found" or "invalid program for execution" errors

Steps to Reproduce

use litesvm::LiteSVM;
use solana_sdk::{
    bpf_loader_upgradeable,
    signature::{Keypair, Signer},
    transaction::Transaction,
};

let mut svm = LiteSVM::new();
let payer = Keypair::new();
let program_keypair = Keypair::new();
let program_id = program_keypair.pubkey();

svm.airdrop(&payer.pubkey(), 10_000_000_000).unwrap();

// Deploy a program using BPF Loader Upgradeable
let program_bytes = std::fs::read("my_program.so").unwrap();
let deploy_tx = Transaction::new_signed_with_payer(
    &bpf_loader_upgradeable::deploy_with_max_program_len(
        &payer.pubkey(),
        &program_id,
        &payer.pubkey(),
        1_000_000_000,
        program_bytes.len(),
    ).unwrap(),
    Some(&payer.pubkey()),
    &[&payer, &program_keypair],
    svm.latest_blockhash(),
);

// Deployment succeeds
svm.send_transaction(deploy_tx).unwrap();

// But calling the program immediately after fails
let call_tx = Transaction::new_signed_with_payer(
    &[Instruction::new_with_bytes(program_id, &[], vec![])],
    Some(&payer.pubkey()),
    &[&payer],
    svm.latest_blockhash(),
);

// ❌ This fails with InvalidProgramForExecution
// because the program account's executable flag is not set
svm.send_transaction(call_tx).unwrap();

Expected Behavior

Programs deployed via BPF Loader Upgradeable should be immediately available for execution, just like programs added via add_program().

Actual Behavior

  • The Program account is created but executable flag is false
  • The program is not loaded into the program cache
  • Calling the program fails with InvalidProgramForExecution

Root Cause

Issue 1: Executable flag not set during deployment

When a program is deployed via bpf_loader_upgradeable, the Program account's executable flag is not set in the TransactionContext. The flag is only set by the loader internally during the deployment process, but when accounts are synced via execute_tx_helper(), the flag hasn't been applied to the account data.

Current code in execute_tx_helper():

let post_accounts = accounts
    .into_iter()
    .enumerate()
    .filter_map(|(idx, pair)| msg.is_writable(idx).then_some(pair))
    .collect();

This only syncs writable accounts, so:

  • ✅ ProgramData account is synced (it's writable)
  • ❌ Program account is NOT synced (it's not writable in the message)

Even if we synced the Program account, its executable flag wouldn't be set yet.

Issue 2: Auto-loading doesn't detect unexecutable programs

The current add_account() implementation in accounts_db.rs:83-104 only loads programs if they're marked executable:

pub(crate) fn add_account(...) -> Result<(), LiteSVMError> {
    if account.executable() && ... {
        let loaded_program = self.load_program(&account)?;
        self.programs_cache.replenish(pubkey, Arc::new(loaded_program));
    }
    // ...
}

Since the Program account's executable flag is false, it's not loaded into the cache.

Proposed Solution

Part 1: Fix executable flag for BPF Upgradeable Program accounts

In accounts_db.rs::sync_accounts(), detect BPF Upgradeable Program accounts and set their executable flag:

pub(crate) fn sync_accounts(
    &mut self,
    mut accounts: Vec<(Pubkey, AccountSharedData)>,
) -> Result<(), LiteSVMError> {
    // ... existing code ...

    for (pubkey, mut acc) in accounts {
        // Check if this is a BPF Upgradeable Program account
        if acc.owner() == &bpf_loader_upgradeable::id() && !acc.executable() {
            if let Ok(UpgradeableLoaderState::Program { .. }) = acc.state() {
                acc.set_executable(true);
            }
        }

        self.add_account(pubkey, acc)?;
    }

    Ok(())
}

Part 2: Sync BPF loader accounts even when not writable

In lib.rs::execute_tx_helper(), ensure BPF loader accounts are synced:

let post_accounts: Vec<(Pubkey, AccountSharedData)> = accounts
    .into_iter()
    .enumerate()
    .filter_map(|(idx, pair)| {
        let is_writable = msg.is_writable(idx);

        // Also sync BPF loader accounts to ensure programs are available
        let is_bpf_loader_account = pair.1.owner() == &bpf_loader_upgradeable::id()
            || pair.1.owner() == &bpf_loader::id();

        if is_writable || is_bpf_loader_account {
            Some(pair)
        } else {
            None
        }
    })
    .collect();

Part 3: Auto-load programs referenced in transactions

In lib.rs::process_transaction(), before executing instructions, check if programs are in cache:

// After cloning program_cache_for_tx_batch
for instruction in message.instructions().iter() {
    let program_id = &account_keys[instruction.program_id_index as usize];

    if program_cache_for_tx_batch.find(program_id).is_none() {
        if let Some(program_account) = self.accounts.get_account(program_id) {
            if program_account.executable() {
                if let Ok(loaded_program) = self.accounts.load_program(&program_account) {
                    program_cache_for_tx_batch.replenish(*program_id, Arc::new(loaded_program));
                }
            }
        }
    }
}

Benefits

  1. Seamless program deployment: Programs deployed via upgradeable loader work immediately
  2. Matches Solana behavior: More closely matches actual Solana runtime behavior
  3. Better testing: Enables realistic integration tests that deploy and call programs
  4. No breaking changes: Purely additive functionality

Testing

I have comprehensive tests demonstrating:

  1. Program deployment and immediate execution
  2. Auto-loading of programs
  3. CPI-created account synchronization

I can provide these tests in a PR if this approach looks good to the maintainers.

Additional Context

This issue affects projects using LiteSVM for testing programs that are deployed via the upgradeable loader (which is the standard deployment method). The workaround is to use add_program() instead of actual deployment, but this doesn't test the full deployment flow.


Version: litesvm v0.8.1 (master)
Impact: Testing of realistic program deployment workflows
Priority: Medium (has workaround, but limits testing realism)

## Proposed Fix

I have a working fix for this issue with comprehensive tests. Would the maintainers be interested in a PR? I can submit it if this approach looks good.

The fix involves:
1. Auto-detecting and marking BPF V3 Program accounts as executable during sync
2. Auto-loading programs referenced in transactions
3. Syncing BPF loader accounts even when not writable

All changes are non-breaking and purely additive.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions