Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/litesvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ solana-instruction.workspace = true
solana-instructions-sysvar.workspace = true
solana-keypair.workspace = true
solana-last-restart-slot.workspace = true
solana-loader-v3-interface.workspace = true
solana-loader-v3-interface = { workspace = true, features = ["bincode"] }
solana-loader-v4-interface.workspace = true
solana-message.workspace = true
solana-native-token.workspace = true
Expand Down
46 changes: 44 additions & 2 deletions crates/litesvm/src/accounts_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,55 @@ impl AccountsDb {
x.1.owner() == &bpf_loader_upgradeable::id()
&& x.1.data().first().is_some_and(|byte| *byte == 3)
});
for (address, acc) in accounts {

for (address, mut acc) in accounts {
// For BPF Loader Upgradeable V3 program accounts, the executable flag may not be set
// during deployment. We need to check if this is a Program account and manually set executable=true
if acc.owner() == &bpf_loader_upgradeable::id() && !acc.executable() {
if let Ok(UpgradeableLoaderState::Program { .. }) = acc.state() {
// This is a Program account - ensure it's marked executable
acc.set_executable(true);
}
}

self.add_account(address, acc)?;
}

Ok(())
}

/// Loads all existing executable programs into the program cache.
///
/// This scans the account database for executable program accounts that haven't been
/// loaded into the program cache and loads them. This is useful when:
/// - Programs are deployed but only their ProgramData accounts appear in ExecutionRecord
/// - Restoring state where programs exist as accounts but aren't cached
/// - Ensuring all programs are available for execution after account sync
pub(crate) fn load_all_existing_programs(&mut self) -> Result<(), LiteSVMError> {
let accounts_snapshot: Vec<(Address, AccountSharedData)> = self
.inner
.iter()
.filter(|(pubkey, acc)| {
let is_executable = acc.executable();
let is_loadable_program = acc.owner() == &bpf_loader_upgradeable::id()
|| acc.owner() == &bpf_loader::id()
|| acc.owner() == &bpf_loader_deprecated::id();
let in_cache = self.programs_cache.find(pubkey).is_some();
is_executable && is_loadable_program && !in_cache
})
.map(|(k, v)| (*k, v.clone()))
.collect();

for (program_pubkey, program_acc) in accounts_snapshot {
let loaded_program = self.load_program(&program_acc)?;
self.programs_cache
.replenish(program_pubkey, Arc::new(loaded_program));
}

Ok(())
}

fn load_program(
pub(crate) fn load_program(
&self,
program_account: &AccountSharedData,
) -> Result<ProgramCacheEntry, InstructionError> {
Expand Down
68 changes: 66 additions & 2 deletions crates/litesvm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,19 @@ impl LiteSVM {
self.add_program_internal::<true>(program_id, program_bytes, loader_id)
}

/// Loads all existing executable programs into the program cache.
///
/// This should be called during initialization to ensure programs that exist in the
/// account database (e.g., from previous test runs or deployed via upgradeable loader)
/// are available for execution.
///
/// This is particularly important when using LiteSVM with persistent state across
/// test runs, as programs deployed in previous runs will exist as accounts but may
/// not be loaded into the program cache.
pub fn load_existing_programs(&mut self) -> Result<(), LiteSVMError> {
self.accounts.load_all_existing_programs()
}

fn create_transaction_context(
&self,
compute_budget: ComputeBudget,
Expand Down Expand Up @@ -1086,6 +1099,31 @@ impl LiteSVM {
let mut program_cache_for_tx_batch = self.accounts.programs_cache.clone();
let mut accumulated_consume_units = 0;
let account_keys = message.account_keys();

// Auto-load any programs referenced in this transaction that aren't in the cache
// This handles the case where upgradeable programs were deployed but their program accounts
// weren't synced into the cache (because only programdata was in ExecutionRecord)
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() {
// Program not in cache - check if it exists in account database
if let Some(program_account) = self.accounts.get_account(program_id) {
if program_account.executable() {
match self.accounts.load_program(&program_account) {
Ok(loaded_program) => {
program_cache_for_tx_batch
.replenish(*program_id, Arc::new(loaded_program));
}
Err(e) => {
log::warn!("Failed to auto-load program {}: {:?}", program_id, e);
}
}
}
}
}
}

let prioritization_fee = compute_budget_limits.get_prioritization_fee();
let fee = solana_fee::calculate_fee(
message,
Expand Down Expand Up @@ -1724,18 +1762,44 @@ fn execute_tx_helper(
) {
let signature = sanitized_tx.signature().to_owned();
let inner_instructions = inner_instructions_list_from_instruction_trace(&ctx);

let ExecutionRecord {
accounts,
return_data,
touched_account_count: _,
accounts_resize_delta: _,
} = ctx.into();

let msg = sanitized_tx.message();
let post_accounts = accounts

let num_message_accounts = msg.account_keys().len();
let post_accounts: Vec<(Address, AccountSharedData)> = accounts
.into_iter()
.enumerate()
.filter_map(|(idx, pair)| msg.is_writable(idx).then_some(pair))
.filter_map(|(idx, pair)| {
// Check if this account was writable in the original message
// For accounts beyond the message (created via CPI), they should always be synced
let is_writable = if idx < num_message_accounts {
msg.is_writable(idx)
} else {
// Account was created during execution (e.g., via bpf_loader_upgradeable deploy)
// Always sync these accounts
true
};

// Also sync BPF loader accounts even if not writable
// This ensures BPF Loader V2/V3 program accounts are synced after deployment
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();

(signature, return_data, inner_instructions, post_accounts)
}

Expand Down
Loading