diff --git a/ledger-tool/src/program.rs b/ledger-tool/src/program.rs index fba9076de2c5bd..c35fe3717cd223 100644 --- a/ledger-tool/src/program.rs +++ b/ledger-tool/src/program.rs @@ -21,11 +21,12 @@ use { }, solana_runtime::bank::Bank, solana_sdk::{ - account::AccountSharedData, + account::{create_account_shared_data_for_test, AccountSharedData}, account_utils::StateMut, bpf_loader_upgradeable::{self, UpgradeableLoaderState}, pubkey::Pubkey, slot_history::Slot, + sysvar, transaction_context::{IndexOfAccount, InstructionAccount}, }, std::{ @@ -510,10 +511,15 @@ pub fn program(ledger_path: &Path, matches: &ArgMatches<'_>) { program_id, // ID of the loaded program. It can modify accounts with the same owner key AccountSharedData::new(0, 0, &loader_id), )); + transaction_accounts.push(( + sysvar::epoch_schedule::id(), + create_account_shared_data_for_test(bank.epoch_schedule()), + )); let interpreted = matches.value_of("mode").unwrap() != "jit"; with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); // Adding `DELAY_VISIBILITY_SLOT_OFFSET` to slots to accommodate for delay visibility of the program +<<<<<<< HEAD let mut loaded_programs = LoadedProgramsForTxBatch::new( bank.slot() + DELAY_VISIBILITY_SLOT_OFFSET, bank.loaded_programs_cache @@ -522,6 +528,10 @@ pub fn program(ledger_path: &Path, matches: &ArgMatches<'_>) { .environments .clone(), ); +======= + let mut loaded_programs = + bank.new_program_cache_for_tx_batch_for_slot(bank.slot() + DELAY_VISIBILITY_SLOT_OFFSET); +>>>>>>> 91b1ee3df6 (Fix: deploy program on last slot of epoch during environment change (#101)) for key in cached_account_keys { loaded_programs.replenish(key, bank.load_program(&key, false, None)); debug!("Loaded program {}", key); diff --git a/program-runtime/src/invoke_context.rs b/program-runtime/src/invoke_context.rs index 5b2d417912256f..8259c2ed2bcc7a 100644 --- a/program-runtime/src/invoke_context.rs +++ b/program-runtime/src/invoke_context.rs @@ -2,7 +2,9 @@ use { crate::{ compute_budget::ComputeBudget, ic_msg, - loaded_programs::{LoadedProgram, LoadedProgramType, LoadedProgramsForTxBatch}, + loaded_programs::{ + LoadedProgram, LoadedProgramType, LoadedProgramsForTxBatch, ProgramRuntimeEnvironments, + }, log_collector::LogCollector, stable_log, sysvar_cache::SysvarCache, @@ -17,8 +19,10 @@ use { vm::{Config, ContextObject, EbpfVm}, }, solana_sdk::{ - account::AccountSharedData, + account::{create_account_shared_data_for_test, AccountSharedData}, bpf_loader_deprecated, + clock::Slot, + epoch_schedule::EpochSchedule, feature_set::FeatureSet, hash::Hash, instruction::{AccountMeta, InstructionError}, @@ -26,6 +30,7 @@ use { pubkey::Pubkey, saturating_add_assign, stable_layout::stable_instruction::StableInstruction, + sysvar, transaction_context::{ IndexOfAccount, InstructionAccount, TransactionAccount, TransactionContext, }, @@ -209,6 +214,17 @@ impl<'a> InvokeContext<'a> { .or_else(|| self.programs_loaded_for_tx_batch.find(pubkey)) } + pub fn get_environments_for_slot( + &self, + effective_slot: Slot, + ) -> Result<&ProgramRuntimeEnvironments, InstructionError> { + let epoch_schedule = self.sysvar_cache.get_epoch_schedule()?; + let epoch = epoch_schedule.get_epoch(effective_slot); + Ok(self + .programs_loaded_for_tx_batch + .get_environments_for_epoch(epoch)) + } + /// Push a stack frame onto the invocation stack pub fn push(&mut self) -> Result<(), InstructionError> { let instruction_context = self @@ -713,6 +729,18 @@ pub fn mock_process_instruction>, slot: Slot, pub environments: ProgramRuntimeEnvironments, + /// Anticipated replacement for `environments` at the next epoch. + /// + /// This is `None` during most of an epoch, and only `Some` around the boundaries (at the end and beginning of an epoch). + /// More precisely, it starts with the recompilation phase a few hundred slots before the epoch boundary, + /// and it ends with the first rerooting after the epoch boundary. + /// Needed when a program is deployed at the last slot of an epoch, becomes effective in the next epoch. + /// So needs to be compiled with the environment for the next epoch. + pub upcoming_environments: Option, + /// The epoch of the last rerooting + pub latest_root_epoch: Epoch, pub hit_max_limit: bool, } impl LoadedProgramsForTxBatch { - pub fn new(slot: Slot, environments: ProgramRuntimeEnvironments) -> Self { + pub fn new( + slot: Slot, + environments: ProgramRuntimeEnvironments, + upcoming_environments: Option, + latest_root_epoch: Epoch, + ) -> Self { Self { entries: HashMap::new(), slot, environments, + upcoming_environments, + latest_root_epoch, + hit_max_limit: false, + } + } + + pub fn new_from_cache( + slot: Slot, + epoch: Epoch, + cache: &ProgramCache, + ) -> Self { + Self { + entries: HashMap::new(), + slot, + environments: cache.get_environments_for_epoch(epoch).clone(), + upcoming_environments: cache.get_upcoming_environments_for_epoch(epoch), + latest_root_epoch: cache.latest_root_epoch, hit_max_limit: false, } } + /// Returns the current environments depending on the given epoch + pub fn get_environments_for_epoch(&self, epoch: Epoch) -> &ProgramRuntimeEnvironments { + if epoch != self.latest_root_epoch { + if let Some(upcoming_environments) = self.upcoming_environments.as_ref() { + return upcoming_environments; + } + } + &self.environments + } + /// Refill the cache with a single entry. It's typically called during transaction loading, and /// transaction processing (for program management instructions). /// It replaces the existing entry (if any) with the provided entry. The return value contains @@ -660,6 +702,17 @@ impl LoadedPrograms { &self.environments } + /// Returns the upcoming environments depending on the given epoch + pub fn get_upcoming_environments_for_epoch( + &self, + epoch: Epoch, + ) -> Option { + if epoch == self.latest_root_epoch { + return self.upcoming_environments.clone(); + } + None + } + /// Insert a single entry. It's typically called during transaction loading, /// when the cache doesn't contain the entry corresponding to program `key`. pub fn assign_program(&mut self, key: Pubkey, entry: Arc) -> bool { @@ -2063,7 +2116,7 @@ mod tests { (program3, (LoadedProgramMatchCriteria::NoCriteria, 3)), (program4, (LoadedProgramMatchCriteria::NoCriteria, 4)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(22, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(22, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 20, 22)); @@ -2079,7 +2132,7 @@ mod tests { (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program4, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(15, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(15, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 15)); @@ -2102,7 +2155,7 @@ mod tests { (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program4, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(18, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(18, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 18)); @@ -2120,7 +2173,7 @@ mod tests { (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program4, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(23, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(23, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 23)); @@ -2138,7 +2191,7 @@ mod tests { (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program4, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(11, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(11, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 11)); @@ -2228,7 +2281,7 @@ mod tests { (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program4, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(21, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(21, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); // Since the fork was pruned, we should not find the entry deployed at slot 20. @@ -2245,7 +2298,7 @@ mod tests { (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program4, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(27, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(27, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 27)); @@ -2277,7 +2330,7 @@ mod tests { (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program4, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(23, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(23, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 23)); @@ -2332,7 +2385,7 @@ mod tests { (program2, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(12, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(12, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 12)); @@ -2352,7 +2405,7 @@ mod tests { ), (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(12, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(12, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program2, 11, 12)); @@ -2418,7 +2471,7 @@ mod tests { (program2, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(19, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(19, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 19)); @@ -2432,7 +2485,7 @@ mod tests { (program2, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(27, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(27, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 27)); @@ -2446,7 +2499,7 @@ mod tests { (program2, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program3, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(22, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(22, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 20, 22)); @@ -2593,7 +2646,7 @@ mod tests { cache.prune(10, 0); let mut missing = vec![(program1, (LoadedProgramMatchCriteria::NoCriteria, 1))]; - let mut extracted = LoadedProgramsForTxBatch::new(20, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(20, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); // The cache should have the program deployed at slot 0 @@ -2637,7 +2690,7 @@ mod tests { (program1, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program2, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(20, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(20, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 20)); @@ -2647,7 +2700,7 @@ mod tests { (program1, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program2, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(6, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(6, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 5, 6)); @@ -2661,7 +2714,7 @@ mod tests { (program1, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program2, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(20, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(20, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 20)); @@ -2671,7 +2724,7 @@ mod tests { (program1, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program2, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(6, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(6, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 6)); @@ -2685,7 +2738,7 @@ mod tests { (program1, (LoadedProgramMatchCriteria::NoCriteria, 1)), (program2, (LoadedProgramMatchCriteria::NoCriteria, 1)), ]; - let mut extracted = LoadedProgramsForTxBatch::new(20, cache.environments.clone()); + let mut extracted = LoadedProgramsForTxBatch::new(20, cache.environments.clone(), None, 0); cache.extract(&mut missing, &mut extracted, true); assert!(match_slot(&extracted, &program1, 0, 20)); diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index 43bff889b0aff8..665d90ac771e49 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -52,7 +52,7 @@ use { rc::Rc, sync::{atomic::Ordering, Arc}, }, - syscalls::create_program_runtime_environment_v1, + syscalls::{create_program_runtime_environment_v1, morph_into_deployment_environment_v1}, }; pub const DEFAULT_LOADER_COMPUTE_UNITS: u64 = 570; @@ -109,11 +109,16 @@ macro_rules! deploy_program { $account_size:expr, $slot:expr, $drop:expr, $new_programdata:expr $(,)?) => {{ let mut load_program_metrics = LoadProgramMetrics::default(); let mut register_syscalls_time = Measure::start("register_syscalls_time"); - let deployment_program_runtime_environment = create_program_runtime_environment_v1( - &$invoke_context.feature_set, - $invoke_context.get_compute_budget(), - true, /* deployment */ - false, /* debugging_features */ + let deployment_slot: Slot = $slot; + let environments = $invoke_context.get_environments_for_slot( + deployment_slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET) + ).map_err(|e| { + // This will never fail since the epoch schedule is already configured. + ic_msg!($invoke_context, "Failed to get runtime environment: {}", e); + InstructionError::ProgramEnvironmentSetupFailure + })?; + let deployment_program_runtime_environment = morph_into_deployment_environment_v1( + environments.program_runtime_v1.clone(), ).map_err(|e| { ic_msg!($invoke_context, "Failed to register syscalls: {}", e); InstructionError::ProgramEnvironmentSetupFailure @@ -146,7 +151,7 @@ macro_rules! deploy_program { $loader_key, $account_size, $slot, - $invoke_context.programs_modified_by_tx.environments.program_runtime_v1.clone(), + environments.program_runtime_v1.clone(), true, )?; if let Some(old_entry) = $invoke_context.find_program_in_cache(&$program_id) { @@ -1545,6 +1550,7 @@ mod tests { }, account_utils::StateMut, clock::Clock, + epoch_schedule::EpochSchedule, instruction::{AccountMeta, InstructionError}, pubkey::Pubkey, rent::Rent, @@ -3732,7 +3738,10 @@ mod tests { #[test] fn test_program_usage_count_on_upgrade() { - let transaction_accounts = vec![]; + let transaction_accounts = vec![( + sysvar::epoch_schedule::id(), + create_account_for_test(&EpochSchedule::default()), + )]; with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); let program_id = Pubkey::new_unique(); let env = Arc::new(BuiltinProgram::new_mock()); @@ -3773,7 +3782,10 @@ mod tests { #[test] fn test_program_usage_count_on_non_upgrade() { - let transaction_accounts = vec![]; + let transaction_accounts = vec![( + sysvar::epoch_schedule::id(), + create_account_for_test(&EpochSchedule::default()), + )]; with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); let program_id = Pubkey::new_unique(); let env = Arc::new(BuiltinProgram::new_mock()); diff --git a/programs/bpf_loader/src/syscalls/mod.rs b/programs/bpf_loader/src/syscalls/mod.rs index d9c66f24e503ed..4a166fa1cf9996 100644 --- a/programs/bpf_loader/src/syscalls/mod.rs +++ b/programs/bpf_loader/src/syscalls/mod.rs @@ -239,6 +239,24 @@ macro_rules! register_feature_gated_function { }; } +pub fn morph_into_deployment_environment_v1( + from: Arc>, +) -> Result, Error> { + let mut config = *from.get_config(); + config.reject_broken_elfs = true; + + let mut result = FunctionRegistry::>::default(); + + for (key, (name, value)) in from.get_function_registry().iter() { + // Deployment of programs with sol_alloc_free is disabled. So do not register the syscall. + if name != *b"sol_alloc_free_" { + result.register_function(key, name, value)?; + } + } + + Ok(BuiltinProgram::new_loader(config, result)) +} + pub fn create_program_runtime_environment_v1<'a>( feature_set: &FeatureSet, compute_budget: &ComputeBudget, diff --git a/programs/loader-v4/src/lib.rs b/programs/loader-v4/src/lib.rs index 6afc03067f7bcc..8b99addd45437c 100644 --- a/programs/loader-v4/src/lib.rs +++ b/programs/loader-v4/src/lib.rs @@ -405,17 +405,21 @@ pub fn process_instruction_deploy( let deployment_slot = state.slot; let effective_slot = deployment_slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET); + let environments = invoke_context + .get_environments_for_slot(effective_slot) + .map_err(|err| { + // This will never fail since the epoch schedule is already configured. + ic_logger_msg!(log_collector, "Failed to get runtime environment {}", err); + InstructionError::InvalidArgument + })?; + let mut load_program_metrics = LoadProgramMetrics { program_id: buffer.get_key().to_string(), ..LoadProgramMetrics::default() }; let executor = LoadedProgram::new( &loader_v4::id(), - invoke_context - .programs_modified_by_tx - .environments - .program_runtime_v2 - .clone(), + environments.program_runtime_v2.clone(), deployment_slot, effective_slot, None, diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index cf4f56edabe8cc..6beb1174f2f39c 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -33,10 +33,6 @@ //! It offers a high-level API that signs transactions //! on behalf of the caller, and a low-level API for when they have //! already been signed and verified. -#[cfg(feature = "dev-context-only-utils")] -use solana_accounts_db::accounts_db::{ - ACCOUNTS_DB_CONFIG_FOR_BENCHMARKS, ACCOUNTS_DB_CONFIG_FOR_TESTING, -}; #[allow(deprecated)] use solana_sdk::recent_blockhashes_account; pub use solana_sdk::reward_type::RewardType; @@ -204,6 +200,13 @@ use { time::{Duration, Instant}, }, }; +#[cfg(feature = "dev-context-only-utils")] +use { + solana_accounts_db::accounts_db::{ + ACCOUNTS_DB_CONFIG_FOR_BENCHMARKS, ACCOUNTS_DB_CONFIG_FOR_TESTING, + }, + solana_program_runtime::loaded_programs::LoadedProgramsForTxBatch, +}; /// params to `verify_accounts_hash` struct VerifyAccountsHashConfig { @@ -8333,6 +8336,14 @@ impl Bank { pub fn update_accounts_hash_for_tests(&self) -> AccountsHash { self.update_accounts_hash(CalcAccountsHashDataSource::IndexForTests, false, false) } + + pub fn new_program_cache_for_tx_batch_for_slot(&self, slot: Slot) -> LoadedProgramsForTxBatch { + LoadedProgramsForTxBatch::new_from_cache( + slot, + self.epoch_schedule.get_epoch(slot), + &self.program_cache.read().unwrap(), + ) + } } /// Compute how much an account has changed size. This function is useful when the data size delta diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 4a57e21415b615..687ff8200d2717 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -14123,3 +14123,247 @@ fn test_failed_simulation_compute_units() { let simulation = bank.simulate_transaction(&sanitized, false); assert_eq!(expected_consumed_units, simulation.units_consumed); } +<<<<<<< HEAD +======= + +#[test] +fn test_filter_program_errors_and_collect_fee_details() { + // TX | EXECUTION RESULT | is nonce | COLLECT | ADDITIONAL | COLLECT + // | | | (TX_FEE, PRIO_FEE) | WITHDRAW FROM PAYER | RESULT + // ------------------------------------------------------------------------------------------------------ + // tx1 | not executed | n/a | (0 , 0) | 0 | Original Err + // tx2 | executed and no error | n/a | (5_000, 1_000) | 0 | Ok + // tx3 | executed has error | true | (5_000, 1_000) | 0 | Ok + // tx4 | executed has error | false | (5_000, 1_000) | 6_000 | Ok + // tx5 | executed error, + // payer insufficient fund | false | (0 , 0) | 0 | InsufficientFundsForFee + // + let initial_payer_balance = 7_000; + let additional_payer_withdraw = 6_000; + let expected_collected_fee_details = CollectorFeeDetails { + transaction_fee: 15_000, + priority_fee: 3_000, + }; + let expected_collect_results = vec![ + Err(TransactionError::AccountNotFound), + Ok(()), + Ok(()), + Ok(()), + Err(TransactionError::InsufficientFundsForFee), + ]; + + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(initial_payer_balance, &Pubkey::new_unique(), 3); + genesis_config.fee_rate_governor = FeeRateGovernor::new(5000, 0); + let bank = Bank::new_for_tests(&genesis_config); + + let tx = SanitizedTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer( + &[ + system_instruction::transfer(&mint_keypair.pubkey(), &Pubkey::new_unique(), 2), + ComputeBudgetInstruction::set_compute_unit_limit(1_000), + ComputeBudgetInstruction::set_compute_unit_price(1_000_000), + ], + Some(&mint_keypair.pubkey()), + &[&mint_keypair], + genesis_config.hash(), + )); + let txs = vec![tx.clone(), tx.clone(), tx.clone(), tx.clone(), tx]; + + let results = vec![ + TransactionExecutionResult::NotExecuted(TransactionError::AccountNotFound), + new_execution_result(Ok(()), None), + new_execution_result( + Err(TransactionError::InstructionError( + 1, + SystemError::ResultWithNegativeLamports.into(), + )), + Some(&NonceFull::default()), + ), + new_execution_result( + Err(TransactionError::InstructionError( + 1, + SystemError::ResultWithNegativeLamports.into(), + )), + None, + ), + new_execution_result(Err(TransactionError::AccountNotFound), None), + ]; + + let results = bank.filter_program_errors_and_collect_fee_details(&txs, &results); + + assert_eq!( + expected_collected_fee_details, + *bank.collector_fee_details.read().unwrap() + ); + assert_eq!( + initial_payer_balance - additional_payer_withdraw, + bank.get_balance(&mint_keypair.pubkey()) + ); + assert_eq!(expected_collect_results, results); +} + +#[test] +fn test_check_execution_status_and_charge_fee() { + let fee = 5000; + let initial_balance = fee - 1000; + let tx_error = + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature); + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(initial_balance, &Pubkey::new_unique(), 3); + genesis_config.fee_rate_governor = FeeRateGovernor::new(5000, 0); + let bank = Bank::new_for_tests(&genesis_config); + let message = new_sanitized_message(Message::new( + &[system_instruction::transfer( + &mint_keypair.pubkey(), + &Pubkey::new_unique(), + 1, + )], + Some(&mint_keypair.pubkey()), + )); + + [Ok(()), Err(tx_error)] + .iter() + .flat_map(|result| [true, false].iter().map(move |is_nonce| (result, is_nonce))) + .for_each(|(result, is_nonce)| { + if result.is_err() && !is_nonce { + assert_eq!( + Err(TransactionError::InsufficientFundsForFee), + bank.check_execution_status_and_charge_fee(&message, result, *is_nonce, fee) + ); + assert_eq!(initial_balance, bank.get_balance(&mint_keypair.pubkey())); + + let small_fee = 1; + assert!(bank + .check_execution_status_and_charge_fee(&message, result, *is_nonce, small_fee) + .is_ok()); + assert_eq!( + initial_balance - small_fee, + bank.get_balance(&mint_keypair.pubkey()) + ); + } else { + assert!(bank + .check_execution_status_and_charge_fee(&message, result, *is_nonce, fee) + .is_ok()); + assert_eq!(initial_balance, bank.get_balance(&mint_keypair.pubkey())); + } + }); +} +#[test] +fn test_deploy_last_epoch_slot() { + solana_logger::setup(); + + // Bank Setup + let (mut genesis_config, mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + genesis_config + .accounts + .remove(&feature_set::reject_callx_r10::id()); + let mut bank = Bank::new_for_tests(&genesis_config); + bank.activate_feature(&feature_set::reject_callx_r10::id()); + + // go to the last slot in the epoch + let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); + let slots_in_epoch = bank.epoch_schedule().get_slots_in_epoch(0); + let bank = new_bank_from_parent_with_bank_forks( + &bank_forks, + bank, + &Pubkey::default(), + slots_in_epoch - 1, + ); + eprintln!("now at slot {} epoch {}", bank.slot(), bank.epoch()); + + // deploy a program + let payer_keypair = Keypair::new(); + let program_keypair = Keypair::new(); + let mut file = File::open("../programs/bpf_loader/test_elfs/out/noop_aligned.so").unwrap(); + let mut elf = Vec::new(); + file.read_to_end(&mut elf).unwrap(); + let min_program_balance = + bank.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()); + let min_buffer_balance = bank + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_buffer(elf.len())); + let min_programdata_balance = bank.get_minimum_balance_for_rent_exemption( + UpgradeableLoaderState::size_of_programdata(elf.len()), + ); + let buffer_address = Pubkey::new_unique(); + let (programdata_address, _) = Pubkey::find_program_address( + &[program_keypair.pubkey().as_ref()], + &bpf_loader_upgradeable::id(), + ); + let upgrade_authority_keypair = Keypair::new(); + + let buffer_account = { + let mut account = AccountSharedData::new( + min_buffer_balance, + UpgradeableLoaderState::size_of_buffer(elf.len()), + &bpf_loader_upgradeable::id(), + ); + account + .set_state(&UpgradeableLoaderState::Buffer { + authority_address: Some(upgrade_authority_keypair.pubkey()), + }) + .unwrap(); + account + .data_as_mut_slice() + .get_mut(UpgradeableLoaderState::size_of_buffer_metadata()..) + .unwrap() + .copy_from_slice(&elf); + account + }; + + let payer_base_balance = LAMPORTS_PER_SOL; + let deploy_fees = { + let fee_calculator = genesis_config.fee_rate_governor.create_fee_calculator(); + 3 * fee_calculator.lamports_per_signature + }; + let min_payer_balance = min_program_balance + .saturating_add(min_programdata_balance) + .saturating_sub(min_buffer_balance.saturating_add(deploy_fees)); + bank.store_account( + &payer_keypair.pubkey(), + &AccountSharedData::new( + payer_base_balance.saturating_add(min_payer_balance), + 0, + &system_program::id(), + ), + ); + bank.store_account(&buffer_address, &buffer_account); + bank.store_account(&program_keypair.pubkey(), &AccountSharedData::default()); + bank.store_account(&programdata_address, &AccountSharedData::default()); + let message = Message::new( + &bpf_loader_upgradeable::deploy_with_max_program_len( + &payer_keypair.pubkey(), + &program_keypair.pubkey(), + &buffer_address, + &upgrade_authority_keypair.pubkey(), + min_program_balance, + elf.len(), + ) + .unwrap(), + Some(&payer_keypair.pubkey()), + ); + let signers = &[&payer_keypair, &program_keypair, &upgrade_authority_keypair]; + let transaction = Transaction::new(signers, message.clone(), bank.last_blockhash()); + let ret = bank.process_transaction(&transaction); + assert!(ret.is_ok(), "ret: {:?}", ret); + goto_end_of_slot(bank.clone()); + + // go to the first slot in the new epoch + let bank = + new_bank_from_parent_with_bank_forks(&bank_forks, bank, &Pubkey::default(), slots_in_epoch); + eprintln!("now at slot {} epoch {}", bank.slot(), bank.epoch()); + + let instruction = Instruction::new_with_bytes(program_keypair.pubkey(), &[], Vec::new()); + let message = Message::new(&[instruction], Some(&mint_keypair.pubkey())); + let binding = mint_keypair.insecure_clone(); + let signers = vec![&binding]; + let transaction = Transaction::new(&signers, message, bank.last_blockhash()); + let result_with_feature_enabled = bank.process_transaction(&transaction); + assert_eq!(result_with_feature_enabled, Ok(())); +} +>>>>>>> 91b1ee3df6 (Fix: deploy program on last slot of epoch during environment change (#101)) diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs new file mode 100644 index 00000000000000..b1673cef0b1b11 --- /dev/null +++ b/svm/src/transaction_processor.rs @@ -0,0 +1,2105 @@ +use { + crate::{ + account_loader::{ + load_accounts, LoadedTransaction, TransactionCheckResult, TransactionLoadResult, + }, + account_overrides::AccountOverrides, + transaction_account_state_info::TransactionAccountStateInfo, + transaction_error_metrics::TransactionErrorMetrics, + transaction_results::{ + DurableNonceFee, TransactionExecutionDetails, TransactionExecutionResult, + }, + }, + log::debug, + percentage::Percentage, + solana_measure::measure::Measure, + solana_program_runtime::{ + compute_budget::ComputeBudget, + loaded_programs::{ + ForkGraph, LoadProgramMetrics, LoadedProgram, LoadedProgramMatchCriteria, + LoadedProgramType, LoadedProgramsForTxBatch, ProgramCache, ProgramRuntimeEnvironment, + ProgramRuntimeEnvironments, DELAY_VISIBILITY_SLOT_OFFSET, + }, + log_collector::LogCollector, + message_processor::MessageProcessor, + runtime_config::RuntimeConfig, + sysvar_cache::SysvarCache, + timings::{ExecuteDetailsTimings, ExecuteTimingType, ExecuteTimings}, + }, + solana_sdk::{ + account::{AccountSharedData, ReadableAccount, PROGRAM_OWNERS}, + account_utils::StateMut, + bpf_loader_upgradeable::{self, UpgradeableLoaderState}, + clock::{Epoch, Slot}, + epoch_schedule::EpochSchedule, + feature_set::FeatureSet, + fee::FeeStructure, + hash::Hash, + inner_instruction::{InnerInstruction, InnerInstructionsList}, + instruction::{CompiledInstruction, InstructionError, TRANSACTION_LEVEL_STACK_HEIGHT}, + loader_v4::{self, LoaderV4State, LoaderV4Status}, + message::SanitizedMessage, + native_loader, + pubkey::Pubkey, + rent_collector::RentCollector, + saturating_add_assign, + transaction::{self, SanitizedTransaction, TransactionError}, + transaction_context::{ExecutionRecord, TransactionContext}, + }, + std::{ + cell::RefCell, + collections::{hash_map::Entry, HashMap}, + fmt::{Debug, Formatter}, + rc::Rc, + sync::{atomic::Ordering, Arc, RwLock}, + }, +}; + +/// A list of log messages emitted during a transaction +pub type TransactionLogMessages = Vec; + +pub struct LoadAndExecuteSanitizedTransactionsOutput { + pub loaded_transactions: Vec, + // Vector of results indicating whether a transaction was executed or could not + // be executed. Note executed transactions can still have failed! + pub execution_results: Vec, +} + +/// Configuration of the recording capabilities for transaction execution +#[derive(Copy, Clone)] +pub struct ExecutionRecordingConfig { + pub enable_cpi_recording: bool, + pub enable_log_recording: bool, + pub enable_return_data_recording: bool, +} + +impl ExecutionRecordingConfig { + pub fn new_single_setting(option: bool) -> Self { + ExecutionRecordingConfig { + enable_return_data_recording: option, + enable_log_recording: option, + enable_cpi_recording: option, + } + } +} + +pub trait TransactionProcessingCallback { + fn account_matches_owners(&self, account: &Pubkey, owners: &[Pubkey]) -> Option; + + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option; + + fn get_last_blockhash_and_lamports_per_signature(&self) -> (Hash, u64); + + fn get_rent_collector(&self) -> &RentCollector; + + fn get_feature_set(&self) -> Arc; + + fn check_account_access( + &self, + _message: &SanitizedMessage, + _account_index: usize, + _account: &AccountSharedData, + _error_counters: &mut TransactionErrorMetrics, + ) -> transaction::Result<()> { + Ok(()) + } + + fn get_program_match_criteria(&self, _program: &Pubkey) -> LoadedProgramMatchCriteria { + LoadedProgramMatchCriteria::NoCriteria + } +} + +#[derive(Debug)] +enum ProgramAccountLoadResult { + AccountNotFound, + InvalidAccountData(ProgramRuntimeEnvironment), + ProgramOfLoaderV1orV2(AccountSharedData), + ProgramOfLoaderV3(AccountSharedData, AccountSharedData, Slot), + ProgramOfLoaderV4(AccountSharedData, Slot), +} + +#[derive(AbiExample)] +pub struct TransactionBatchProcessor { + /// Bank slot (i.e. block) + slot: Slot, + + /// Bank epoch + epoch: Epoch, + + /// initialized from genesis + epoch_schedule: EpochSchedule, + + /// Transaction fee structure + fee_structure: FeeStructure, + + /// Optional config parameters that can override runtime behavior + runtime_config: Arc, + + /// SysvarCache is a collection of system variables that are + /// accessible from on chain programs. It is passed to SVM from + /// client code (e.g. Bank) and forwarded to the MessageProcessor. + pub sysvar_cache: RwLock, + + /// Programs required for transaction batch processing + pub program_cache: Arc>>, +} + +impl Debug for TransactionBatchProcessor { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TransactionBatchProcessor") + .field("slot", &self.slot) + .field("epoch", &self.epoch) + .field("epoch_schedule", &self.epoch_schedule) + .field("fee_structure", &self.fee_structure) + .field("runtime_config", &self.runtime_config) + .field("sysvar_cache", &self.sysvar_cache) + .field("program_cache", &self.program_cache) + .finish() + } +} + +impl Default for TransactionBatchProcessor { + fn default() -> Self { + Self { + slot: Slot::default(), + epoch: Epoch::default(), + epoch_schedule: EpochSchedule::default(), + fee_structure: FeeStructure::default(), + runtime_config: Arc::::default(), + sysvar_cache: RwLock::::default(), + program_cache: Arc::new(RwLock::new(ProgramCache::new( + Slot::default(), + Epoch::default(), + ))), + } + } +} + +impl TransactionBatchProcessor { + pub fn new( + slot: Slot, + epoch: Epoch, + epoch_schedule: EpochSchedule, + fee_structure: FeeStructure, + runtime_config: Arc, + program_cache: Arc>>, + ) -> Self { + Self { + slot, + epoch, + epoch_schedule, + fee_structure, + runtime_config, + sysvar_cache: RwLock::::default(), + program_cache, + } + } + + /// Main entrypoint to the SVM. + #[allow(clippy::too_many_arguments)] + pub fn load_and_execute_sanitized_transactions<'a, CB: TransactionProcessingCallback>( + &self, + callbacks: &CB, + sanitized_txs: &[SanitizedTransaction], + check_results: &mut [TransactionCheckResult], + error_counters: &mut TransactionErrorMetrics, + recording_config: ExecutionRecordingConfig, + timings: &mut ExecuteTimings, + account_overrides: Option<&AccountOverrides>, + builtin_programs: impl Iterator, + log_messages_bytes_limit: Option, + limit_to_load_programs: bool, + ) -> LoadAndExecuteSanitizedTransactionsOutput { + let mut program_accounts_map = Self::filter_executable_program_accounts( + callbacks, + sanitized_txs, + check_results, + PROGRAM_OWNERS, + ); + let native_loader = native_loader::id(); + for builtin_program in builtin_programs { + program_accounts_map.insert(*builtin_program, (&native_loader, 0)); + } + + let programs_loaded_for_tx_batch = Rc::new(RefCell::new(self.replenish_program_cache( + callbacks, + &program_accounts_map, + limit_to_load_programs, + ))); + + if programs_loaded_for_tx_batch.borrow().hit_max_limit { + return LoadAndExecuteSanitizedTransactionsOutput { + loaded_transactions: vec![], + execution_results: vec![], + }; + } + + let mut load_time = Measure::start("accounts_load"); + let mut loaded_transactions = load_accounts( + callbacks, + sanitized_txs, + check_results, + error_counters, + &self.fee_structure, + account_overrides, + &program_accounts_map, + &programs_loaded_for_tx_batch.borrow(), + ); + load_time.stop(); + + let mut execution_time = Measure::start("execution_time"); + + let execution_results: Vec = loaded_transactions + .iter_mut() + .zip(sanitized_txs.iter()) + .map(|(accs, tx)| match accs { + (Err(e), _nonce) => TransactionExecutionResult::NotExecuted(e.clone()), + (Ok(loaded_transaction), nonce) => { + let compute_budget = + if let Some(compute_budget) = self.runtime_config.compute_budget { + compute_budget + } else { + let mut compute_budget_process_transaction_time = + Measure::start("compute_budget_process_transaction_time"); + let maybe_compute_budget = ComputeBudget::try_from_instructions( + tx.message().program_instructions_iter(), + ); + compute_budget_process_transaction_time.stop(); + saturating_add_assign!( + timings + .execute_accessories + .compute_budget_process_transaction_us, + compute_budget_process_transaction_time.as_us() + ); + if let Err(err) = maybe_compute_budget { + return TransactionExecutionResult::NotExecuted(err); + } + maybe_compute_budget.unwrap() + }; + + let result = self.execute_loaded_transaction( + callbacks, + tx, + loaded_transaction, + compute_budget, + nonce.as_ref().map(DurableNonceFee::from), + recording_config, + timings, + error_counters, + log_messages_bytes_limit, + &programs_loaded_for_tx_batch.borrow(), + ); + + if let TransactionExecutionResult::Executed { + details, + programs_modified_by_tx, + } = &result + { + // Update batch specific cache of the loaded programs with the modifications + // made by the transaction, if it executed successfully. + if details.status.is_ok() { + programs_loaded_for_tx_batch + .borrow_mut() + .merge(programs_modified_by_tx); + } + } + + result + } + }) + .collect(); + + execution_time.stop(); + + const SHRINK_LOADED_PROGRAMS_TO_PERCENTAGE: u8 = 90; + self.program_cache + .write() + .unwrap() + .evict_using_2s_random_selection( + Percentage::from(SHRINK_LOADED_PROGRAMS_TO_PERCENTAGE), + self.slot, + ); + + debug!( + "load: {}us execute: {}us txs_len={}", + load_time.as_us(), + execution_time.as_us(), + sanitized_txs.len(), + ); + + timings.saturating_add_in_place(ExecuteTimingType::LoadUs, load_time.as_us()); + timings.saturating_add_in_place(ExecuteTimingType::ExecuteUs, execution_time.as_us()); + + LoadAndExecuteSanitizedTransactionsOutput { + loaded_transactions, + execution_results, + } + } + + /// Returns a hash map of executable program accounts (program accounts that are not writable + /// in the given transactions), and their owners, for the transactions with a valid + /// blockhash or nonce. + fn filter_executable_program_accounts<'a, CB: TransactionProcessingCallback>( + callbacks: &CB, + txs: &[SanitizedTransaction], + check_results: &mut [TransactionCheckResult], + program_owners: &'a [Pubkey], + ) -> HashMap { + let mut result: HashMap = HashMap::new(); + check_results.iter_mut().zip(txs).for_each(|etx| { + if let ((Ok(()), _nonce, lamports_per_signature), tx) = etx { + if lamports_per_signature.is_some() { + tx.message() + .account_keys() + .iter() + .for_each(|key| match result.entry(*key) { + Entry::Occupied(mut entry) => { + let (_, count) = entry.get_mut(); + saturating_add_assign!(*count, 1); + } + Entry::Vacant(entry) => { + if let Some(index) = + callbacks.account_matches_owners(key, program_owners) + { + if let Some(owner) = program_owners.get(index) { + entry.insert((owner, 1)); + } + } + } + }); + } else { + // If the transaction's nonce account was not valid, and blockhash is not found, + // the transaction will fail to process. Let's not load any programs from the + // transaction, and update the status of the transaction. + *etx.0 = (Err(TransactionError::BlockhashNotFound), None, None); + } + } + }); + result + } + + /// Load program with a specific pubkey from program cache, and + /// update the program's access slot as a side-effect. + pub fn load_program_with_pubkey( + &self, + callbacks: &CB, + pubkey: &Pubkey, + reload: bool, + effective_epoch: Epoch, + ) -> Arc { + let program_cache = self.program_cache.read().unwrap(); + let environments = program_cache.get_environments_for_epoch(effective_epoch); + let mut load_program_metrics = LoadProgramMetrics { + program_id: pubkey.to_string(), + ..LoadProgramMetrics::default() + }; + + let mut loaded_program = + match self.load_program_accounts(callbacks, pubkey, environments) { + ProgramAccountLoadResult::AccountNotFound => Ok(LoadedProgram::new_tombstone( + self.slot, + LoadedProgramType::Closed, + )), + + ProgramAccountLoadResult::InvalidAccountData(env) => Err((self.slot, env)), + + ProgramAccountLoadResult::ProgramOfLoaderV1orV2(program_account) => { + Self::load_program_from_bytes( + &mut load_program_metrics, + program_account.data(), + program_account.owner(), + program_account.data().len(), + 0, + environments.program_runtime_v1.clone(), + reload, + ) + .map_err(|_| (0, environments.program_runtime_v1.clone())) + } + + ProgramAccountLoadResult::ProgramOfLoaderV3( + program_account, + programdata_account, + slot, + ) => programdata_account + .data() + .get(UpgradeableLoaderState::size_of_programdata_metadata()..) + .ok_or(Box::new(InstructionError::InvalidAccountData).into()) + .and_then(|programdata| { + Self::load_program_from_bytes( + &mut load_program_metrics, + programdata, + program_account.owner(), + program_account + .data() + .len() + .saturating_add(programdata_account.data().len()), + slot, + environments.program_runtime_v1.clone(), + reload, + ) + }) + .map_err(|_| (slot, environments.program_runtime_v1.clone())), + + ProgramAccountLoadResult::ProgramOfLoaderV4(program_account, slot) => { + program_account + .data() + .get(LoaderV4State::program_data_offset()..) + .ok_or(Box::new(InstructionError::InvalidAccountData).into()) + .and_then(|elf_bytes| { + Self::load_program_from_bytes( + &mut load_program_metrics, + elf_bytes, + &loader_v4::id(), + program_account.data().len(), + slot, + environments.program_runtime_v2.clone(), + reload, + ) + }) + .map_err(|_| (slot, environments.program_runtime_v2.clone())) + } + } + .unwrap_or_else(|(slot, env)| { + LoadedProgram::new_tombstone(slot, LoadedProgramType::FailedVerification(env)) + }); + + let mut timings = ExecuteDetailsTimings::default(); + load_program_metrics.submit_datapoint(&mut timings); + if !Arc::ptr_eq( + &environments.program_runtime_v1, + &program_cache.environments.program_runtime_v1, + ) || !Arc::ptr_eq( + &environments.program_runtime_v2, + &program_cache.environments.program_runtime_v2, + ) { + // There can be two entries per program when the environment changes. + // One for the old environment before the epoch boundary and one for the new environment after the epoch boundary. + // These two entries have the same deployment slot, so they must differ in their effective slot instead. + // This is done by setting the effective slot of the entry for the new environment to the epoch boundary. + loaded_program.effective_slot = loaded_program + .effective_slot + .max(self.epoch_schedule.get_first_slot_in_epoch(effective_epoch)); + } + loaded_program.update_access_slot(self.slot); + Arc::new(loaded_program) + } + + fn replenish_program_cache( + &self, + callback: &CB, + program_accounts_map: &HashMap, + limit_to_load_programs: bool, + ) -> LoadedProgramsForTxBatch { + let mut missing_programs: Vec<(Pubkey, (LoadedProgramMatchCriteria, u64))> = + program_accounts_map + .iter() + .map(|(pubkey, (_, count))| { + ( + *pubkey, + (callback.get_program_match_criteria(pubkey), *count), + ) + }) + .collect(); + + let mut loaded_programs_for_txs = None; + let mut program_to_store = None; + loop { + let (program_to_load, task_cookie, task_waiter) = { + // Lock the global cache. + let mut program_cache = self.program_cache.write().unwrap(); + // Initialize our local cache. + let is_first_round = loaded_programs_for_txs.is_none(); + if is_first_round { + loaded_programs_for_txs = Some(LoadedProgramsForTxBatch::new_from_cache( + self.slot, + self.epoch, + &program_cache, + )); + } + // Submit our last completed loading task. + if let Some((key, program)) = program_to_store.take() { + if program_cache.finish_cooperative_loading_task(self.slot, key, program) + && limit_to_load_programs + { + // This branch is taken when there is an error in assigning a program to a + // cache slot. It is not possible to mock this error for SVM unit + // tests purposes. + let mut ret = LoadedProgramsForTxBatch::new_from_cache( + self.slot, + self.epoch, + &program_cache, + ); + ret.hit_max_limit = true; + return ret; + } + } + // Figure out which program needs to be loaded next. + let program_to_load = program_cache.extract( + &mut missing_programs, + loaded_programs_for_txs.as_mut().unwrap(), + is_first_round, + ); + let task_waiter = Arc::clone(&program_cache.loading_task_waiter); + (program_to_load, task_waiter.cookie(), task_waiter) + // Unlock the global cache again. + }; + + if let Some((key, count)) = program_to_load { + // Load, verify and compile one program. + let program = self.load_program_with_pubkey(callback, &key, false, self.epoch); + program.tx_usage_counter.store(count, Ordering::Relaxed); + program_to_store = Some((key, program)); + } else if missing_programs.is_empty() { + break; + } else { + // Sleep until the next finish_cooperative_loading_task() call. + // Once a task completes we'll wake up and try to load the + // missing programs inside the tx batch again. + let _new_cookie = task_waiter.wait(task_cookie); + + // This branch is not tested in the SVM because it requires concurrent threads. + // In addition, one of them must be holding the mutex while the other must be + // trying to lock it. + } + } + + loaded_programs_for_txs.unwrap() + } + + /// Execute a transaction using the provided loaded accounts and update + /// the executors cache if the transaction was successful. + #[allow(clippy::too_many_arguments)] + fn execute_loaded_transaction( + &self, + callback: &CB, + tx: &SanitizedTransaction, + loaded_transaction: &mut LoadedTransaction, + compute_budget: ComputeBudget, + durable_nonce_fee: Option, + recording_config: ExecutionRecordingConfig, + timings: &mut ExecuteTimings, + error_counters: &mut TransactionErrorMetrics, + log_messages_bytes_limit: Option, + programs_loaded_for_tx_batch: &LoadedProgramsForTxBatch, + ) -> TransactionExecutionResult { + let transaction_accounts = std::mem::take(&mut loaded_transaction.accounts); + + fn transaction_accounts_lamports_sum( + accounts: &[(Pubkey, AccountSharedData)], + message: &SanitizedMessage, + ) -> Option { + let mut lamports_sum = 0u128; + for i in 0..message.account_keys().len() { + let (_, account) = accounts.get(i)?; + lamports_sum = lamports_sum.checked_add(u128::from(account.lamports()))?; + } + Some(lamports_sum) + } + + let lamports_before_tx = + transaction_accounts_lamports_sum(&transaction_accounts, tx.message()).unwrap_or(0); + + let mut transaction_context = TransactionContext::new( + transaction_accounts, + callback.get_rent_collector().rent.clone(), + compute_budget.max_invoke_stack_height, + compute_budget.max_instruction_trace_length, + ); + #[cfg(debug_assertions)] + transaction_context.set_signature(tx.signature()); + + let pre_account_state_info = TransactionAccountStateInfo::new( + &callback.get_rent_collector().rent, + &transaction_context, + tx.message(), + ); + + let log_collector = if recording_config.enable_log_recording { + match log_messages_bytes_limit { + None => Some(LogCollector::new_ref()), + Some(log_messages_bytes_limit) => Some(LogCollector::new_ref_with_limit(Some( + log_messages_bytes_limit, + ))), + } + } else { + None + }; + + let (blockhash, lamports_per_signature) = + callback.get_last_blockhash_and_lamports_per_signature(); + + let mut executed_units = 0u64; + let mut programs_modified_by_tx = LoadedProgramsForTxBatch::new( + self.slot, + programs_loaded_for_tx_batch.environments.clone(), + programs_loaded_for_tx_batch.upcoming_environments.clone(), + programs_loaded_for_tx_batch.latest_root_epoch, + ); + let mut process_message_time = Measure::start("process_message_time"); + let process_result = MessageProcessor::process_message( + tx.message(), + &loaded_transaction.program_indices, + &mut transaction_context, + log_collector.clone(), + programs_loaded_for_tx_batch, + &mut programs_modified_by_tx, + callback.get_feature_set(), + compute_budget, + timings, + &self.sysvar_cache.read().unwrap(), + blockhash, + lamports_per_signature, + &mut executed_units, + ); + process_message_time.stop(); + + saturating_add_assign!( + timings.execute_accessories.process_message_us, + process_message_time.as_us() + ); + + let mut status = process_result + .and_then(|info| { + let post_account_state_info = TransactionAccountStateInfo::new( + &callback.get_rent_collector().rent, + &transaction_context, + tx.message(), + ); + TransactionAccountStateInfo::verify_changes( + &pre_account_state_info, + &post_account_state_info, + &transaction_context, + ) + .map(|_| info) + }) + .map_err(|err| { + match err { + TransactionError::InvalidRentPayingAccount + | TransactionError::InsufficientFundsForRent { .. } => { + error_counters.invalid_rent_paying_account += 1; + } + TransactionError::InvalidAccountIndex => { + error_counters.invalid_account_index += 1; + } + _ => { + error_counters.instruction_error += 1; + } + } + err + }); + + let log_messages: Option = + log_collector.and_then(|log_collector| { + Rc::try_unwrap(log_collector) + .map(|log_collector| log_collector.into_inner().into_messages()) + .ok() + }); + + let inner_instructions = if recording_config.enable_cpi_recording { + Some(Self::inner_instructions_list_from_instruction_trace( + &transaction_context, + )) + } else { + None + }; + + let ExecutionRecord { + accounts, + return_data, + touched_account_count, + accounts_resize_delta: accounts_data_len_delta, + } = transaction_context.into(); + + if status.is_ok() + && transaction_accounts_lamports_sum(&accounts, tx.message()) + .filter(|lamports_after_tx| lamports_before_tx == *lamports_after_tx) + .is_none() + { + status = Err(TransactionError::UnbalancedTransaction); + } + let status = status.map(|_| ()); + + loaded_transaction.accounts = accounts; + saturating_add_assign!( + timings.details.total_account_count, + loaded_transaction.accounts.len() as u64 + ); + saturating_add_assign!(timings.details.changed_account_count, touched_account_count); + + let return_data = + if recording_config.enable_return_data_recording && !return_data.data.is_empty() { + Some(return_data) + } else { + None + }; + + TransactionExecutionResult::Executed { + details: TransactionExecutionDetails { + status, + log_messages, + inner_instructions, + durable_nonce_fee, + return_data, + executed_units, + accounts_data_len_delta, + }, + programs_modified_by_tx: Box::new(programs_modified_by_tx), + } + } + + /// Find the slot in which the program was most recently modified. + /// Returns slot 0 for programs deployed with v1/v2 loaders, since programs deployed + /// with those loaders do not retain deployment slot information. + /// Returns an error if the program's account state can not be found or parsed. + pub fn program_modification_slot( + &self, + callbacks: &CB, + pubkey: &Pubkey, + ) -> transaction::Result { + let program = callbacks + .get_account_shared_data(pubkey) + .ok_or(TransactionError::ProgramAccountNotFound)?; + if bpf_loader_upgradeable::check_id(program.owner()) { + if let Ok(UpgradeableLoaderState::Program { + programdata_address, + }) = program.state() + { + let programdata = callbacks + .get_account_shared_data(&programdata_address) + .ok_or(TransactionError::ProgramAccountNotFound)?; + if let Ok(UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address: _, + }) = programdata.state() + { + return Ok(slot); + } + } + Err(TransactionError::ProgramAccountNotFound) + } else if loader_v4::check_id(program.owner()) { + let state = solana_loader_v4_program::get_state(program.data()) + .map_err(|_| TransactionError::ProgramAccountNotFound)?; + Ok(state.slot) + } else { + Ok(0) + } + } + + fn load_program_from_bytes( + load_program_metrics: &mut LoadProgramMetrics, + programdata: &[u8], + loader_key: &Pubkey, + account_size: usize, + deployment_slot: Slot, + program_runtime_environment: ProgramRuntimeEnvironment, + reloading: bool, + ) -> std::result::Result> { + if reloading { + // Safety: this is safe because the program is being reloaded in the cache. + unsafe { + LoadedProgram::reload( + loader_key, + program_runtime_environment.clone(), + deployment_slot, + deployment_slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET), + programdata, + account_size, + load_program_metrics, + ) + } + } else { + LoadedProgram::new( + loader_key, + program_runtime_environment.clone(), + deployment_slot, + deployment_slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET), + programdata, + account_size, + load_program_metrics, + ) + } + } + + fn load_program_accounts( + &self, + callbacks: &CB, + pubkey: &Pubkey, + environments: &ProgramRuntimeEnvironments, + ) -> ProgramAccountLoadResult { + let program_account = match callbacks.get_account_shared_data(pubkey) { + None => return ProgramAccountLoadResult::AccountNotFound, + Some(account) => account, + }; + + debug_assert!(solana_bpf_loader_program::check_loader_id( + program_account.owner() + )); + + if loader_v4::check_id(program_account.owner()) { + return solana_loader_v4_program::get_state(program_account.data()) + .ok() + .and_then(|state| { + (!matches!(state.status, LoaderV4Status::Retracted)).then_some(state.slot) + }) + .map(|slot| ProgramAccountLoadResult::ProgramOfLoaderV4(program_account, slot)) + .unwrap_or(ProgramAccountLoadResult::InvalidAccountData( + environments.program_runtime_v2.clone(), + )); + } + + if !bpf_loader_upgradeable::check_id(program_account.owner()) { + return ProgramAccountLoadResult::ProgramOfLoaderV1orV2(program_account); + } + + if let Ok(UpgradeableLoaderState::Program { + programdata_address, + }) = program_account.state() + { + let programdata_account = match callbacks.get_account_shared_data(&programdata_address) + { + None => return ProgramAccountLoadResult::AccountNotFound, + Some(account) => account, + }; + + if let Ok(UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address: _, + }) = programdata_account.state() + { + return ProgramAccountLoadResult::ProgramOfLoaderV3( + program_account, + programdata_account, + slot, + ); + } + } + ProgramAccountLoadResult::InvalidAccountData(environments.program_runtime_v1.clone()) + } + + /// Extract the InnerInstructionsList from a TransactionContext + fn inner_instructions_list_from_instruction_trace( + transaction_context: &TransactionContext, + ) -> InnerInstructionsList { + debug_assert!(transaction_context + .get_instruction_context_at_index_in_trace(0) + .map(|instruction_context| instruction_context.get_stack_height() + == TRANSACTION_LEVEL_STACK_HEIGHT) + .unwrap_or(true)); + let mut outer_instructions = Vec::new(); + for index_in_trace in 0..transaction_context.get_instruction_trace_length() { + if let Ok(instruction_context) = + transaction_context.get_instruction_context_at_index_in_trace(index_in_trace) + { + let stack_height = instruction_context.get_stack_height(); + if stack_height == TRANSACTION_LEVEL_STACK_HEIGHT { + outer_instructions.push(Vec::new()); + } else if let Some(inner_instructions) = outer_instructions.last_mut() { + let stack_height = u8::try_from(stack_height).unwrap_or(u8::MAX); + let instruction = CompiledInstruction::new_from_raw_parts( + instruction_context + .get_index_of_program_account_in_transaction( + instruction_context + .get_number_of_program_accounts() + .saturating_sub(1), + ) + .unwrap_or_default() as u8, + instruction_context.get_instruction_data().to_vec(), + (0..instruction_context.get_number_of_instruction_accounts()) + .map(|instruction_account_index| { + instruction_context + .get_index_of_instruction_account_in_transaction( + instruction_account_index, + ) + .unwrap_or_default() as u8 + }) + .collect(), + ); + inner_instructions.push(InnerInstruction { + instruction, + stack_height, + }); + } else { + debug_assert!(false); + } + } else { + debug_assert!(false); + } + } + outer_instructions + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_program_runtime::{ + loaded_programs::BlockRelation, solana_rbpf::program::BuiltinProgram, + }, + solana_sdk::{ + account::WritableAccount, + bpf_loader, + message::{LegacyMessage, Message, MessageHeader}, + rent_debits::RentDebits, + signature::{Keypair, Signature}, + sysvar::rent::Rent, + transaction::{SanitizedTransaction, Transaction, TransactionError}, + transaction_context::TransactionContext, + }, + std::{ + env, + fs::{self, File}, + io::Read, + }, + }; + + struct TestForkGraph {} + + impl ForkGraph for TestForkGraph { + fn relationship(&self, _a: Slot, _b: Slot) -> BlockRelation { + BlockRelation::Unknown + } + } + + #[derive(Default, Clone)] + pub struct MockBankCallback { + rent_collector: RentCollector, + feature_set: Arc, + pub account_shared_data: HashMap, + } + + impl TransactionProcessingCallback for MockBankCallback { + fn account_matches_owners(&self, account: &Pubkey, owners: &[Pubkey]) -> Option { + if let Some(data) = self.account_shared_data.get(account) { + if data.lamports() == 0 { + None + } else { + owners.iter().position(|entry| data.owner() == entry) + } + } else { + None + } + } + + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option { + self.account_shared_data.get(pubkey).cloned() + } + + fn get_last_blockhash_and_lamports_per_signature(&self) -> (Hash, u64) { + (Hash::new_unique(), 2) + } + + fn get_rent_collector(&self) -> &RentCollector { + &self.rent_collector + } + + fn get_feature_set(&self) -> Arc { + self.feature_set.clone() + } + } + + #[test] + fn test_inner_instructions_list_from_instruction_trace() { + let instruction_trace = [1, 2, 1, 1, 2, 3, 2]; + let mut transaction_context = + TransactionContext::new(vec![], Rent::default(), 3, instruction_trace.len()); + for (index_in_trace, stack_height) in instruction_trace.into_iter().enumerate() { + while stack_height <= transaction_context.get_instruction_context_stack_height() { + transaction_context.pop().unwrap(); + } + if stack_height > transaction_context.get_instruction_context_stack_height() { + transaction_context + .get_next_instruction_context() + .unwrap() + .configure(&[], &[], &[index_in_trace as u8]); + transaction_context.push().unwrap(); + } + } + let inner_instructions = + TransactionBatchProcessor::::inner_instructions_list_from_instruction_trace( + &transaction_context, + ); + + assert_eq!( + inner_instructions, + vec![ + vec![InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![1], vec![]), + stack_height: 2, + }], + vec![], + vec![ + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![4], vec![]), + stack_height: 2, + }, + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![5], vec![]), + stack_height: 3, + }, + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![6], vec![]), + stack_height: 2, + }, + ] + ] + ); + } + + #[test] + fn test_load_program_accounts_account_not_found() { + let mut mock_bank = MockBankCallback::default(); + let key = Pubkey::new_unique(); + let environment = ProgramRuntimeEnvironments::default(); + let batch_processor = TransactionBatchProcessor::::default(); + + let result = batch_processor.load_program_accounts(&mock_bank, &key, &environment); + + assert!(matches!(result, ProgramAccountLoadResult::AccountNotFound)); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader_upgradeable::id()); + let state = UpgradeableLoaderState::Program { + programdata_address: Pubkey::new_unique(), + }; + account_data.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.load_program_accounts(&mock_bank, &key, &environment); + assert!(matches!(result, ProgramAccountLoadResult::AccountNotFound)); + + account_data.set_data(Vec::new()); + mock_bank.account_shared_data.insert(key, account_data); + + let result = batch_processor.load_program_accounts(&mock_bank, &key, &environment); + + assert!(matches!( + result, + ProgramAccountLoadResult::InvalidAccountData(_) + )); + } + + #[test] + fn test_load_program_accounts_loader_v4() { + let key = Pubkey::new_unique(); + let mut mock_bank = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(loader_v4::id()); + let environment = ProgramRuntimeEnvironments::default(); + let batch_processor = TransactionBatchProcessor::::default(); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.load_program_accounts(&mock_bank, &key, &environment); + assert!(matches!( + result, + ProgramAccountLoadResult::InvalidAccountData(_) + )); + + account_data.set_data(vec![0; 64]); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + let result = batch_processor.load_program_accounts(&mock_bank, &key, &environment); + assert!(matches!( + result, + ProgramAccountLoadResult::InvalidAccountData(_) + )); + + let loader_data = LoaderV4State { + slot: 25, + authority_address: Pubkey::new_unique(), + status: LoaderV4Status::Deployed, + }; + let encoded = unsafe { + std::mem::transmute::<&LoaderV4State, &[u8; LoaderV4State::program_data_offset()]>( + &loader_data, + ) + }; + account_data.set_data(encoded.to_vec()); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.load_program_accounts(&mock_bank, &key, &environment); + + match result { + ProgramAccountLoadResult::ProgramOfLoaderV4(data, slot) => { + assert_eq!(data, account_data); + assert_eq!(slot, 25); + } + + _ => panic!("Invalid result"), + } + } + + #[test] + fn test_load_program_accounts_loader_v1_or_v2() { + let key = Pubkey::new_unique(); + let mut mock_bank = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader::id()); + let environment = ProgramRuntimeEnvironments::default(); + let batch_processor = TransactionBatchProcessor::::default(); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.load_program_accounts(&mock_bank, &key, &environment); + match result { + ProgramAccountLoadResult::ProgramOfLoaderV1orV2(data) => { + assert_eq!(data, account_data); + } + _ => panic!("Invalid result"), + } + } + + #[test] + fn test_load_program_accounts_success() { + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let mut mock_bank = MockBankCallback::default(); + let environment = ProgramRuntimeEnvironments::default(); + let batch_processor = TransactionBatchProcessor::::default(); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader_upgradeable::id()); + + let state = UpgradeableLoaderState::Program { + programdata_address: key2, + }; + account_data.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .insert(key1, account_data.clone()); + + let state = UpgradeableLoaderState::ProgramData { + slot: 25, + upgrade_authority_address: None, + }; + let mut account_data2 = AccountSharedData::default(); + account_data2.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .insert(key2, account_data2.clone()); + + let result = batch_processor.load_program_accounts(&mock_bank, &key1, &environment); + + match result { + ProgramAccountLoadResult::ProgramOfLoaderV3(data1, data2, slot) => { + assert_eq!(data1, account_data); + assert_eq!(data2, account_data2); + assert_eq!(slot, 25); + } + + _ => panic!("Invalid result"), + } + } + + fn load_test_program() -> Vec { + let mut dir = env::current_dir().unwrap(); + dir.push("tests"); + dir.push("example-programs"); + dir.push("hello-solana"); + dir.push("hello_solana_program.so"); + let mut file = File::open(dir.clone()).expect("file not found"); + let metadata = fs::metadata(dir).expect("Unable to read metadata"); + let mut buffer = vec![0; metadata.len() as usize]; + file.read_exact(&mut buffer).expect("Buffer overflow"); + buffer + } + + #[test] + fn test_load_program_from_bytes() { + let buffer = load_test_program(); + + let mut metrics = LoadProgramMetrics::default(); + let loader = bpf_loader_upgradeable::id(); + let size = buffer.len(); + let slot = 2; + let environment = ProgramRuntimeEnvironment::new(BuiltinProgram::new_mock()); + + let result = TransactionBatchProcessor::::load_program_from_bytes( + &mut metrics, + &buffer, + &loader, + size, + slot, + environment.clone(), + false, + ); + + assert!(result.is_ok()); + + let result = TransactionBatchProcessor::::load_program_from_bytes( + &mut metrics, + &buffer, + &loader, + size, + slot, + environment, + true, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_load_program_not_found() { + let mock_bank = MockBankCallback::default(); + let key = Pubkey::new_unique(); + let batch_processor = TransactionBatchProcessor::::default(); + + let result = batch_processor.load_program_with_pubkey(&mock_bank, &key, false, 50); + + let loaded_program = LoadedProgram::new_tombstone(0, LoadedProgramType::Closed); + assert_eq!(result, Arc::new(loaded_program)); + } + + #[test] + fn test_load_program_invalid_account_data() { + let key = Pubkey::new_unique(); + let mut mock_bank = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(loader_v4::id()); + let batch_processor = TransactionBatchProcessor::::default(); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.load_program_with_pubkey(&mock_bank, &key, false, 20); + + let loaded_program = LoadedProgram::new_tombstone( + 0, + LoadedProgramType::FailedVerification( + batch_processor + .program_cache + .read() + .unwrap() + .get_environments_for_epoch(20) + .clone() + .program_runtime_v1, + ), + ); + assert_eq!(result, Arc::new(loaded_program)); + } + + #[test] + fn test_load_program_program_loader_v1_or_v2() { + let key = Pubkey::new_unique(); + let mut mock_bank = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader::id()); + let batch_processor = TransactionBatchProcessor::::default(); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + // This should return an error + let result = batch_processor.load_program_with_pubkey(&mock_bank, &key, false, 20); + let loaded_program = LoadedProgram::new_tombstone( + 0, + LoadedProgramType::FailedVerification( + batch_processor + .program_cache + .read() + .unwrap() + .get_environments_for_epoch(20) + .clone() + .program_runtime_v1, + ), + ); + assert_eq!(result, Arc::new(loaded_program)); + + let buffer = load_test_program(); + account_data.set_data(buffer); + + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.load_program_with_pubkey(&mock_bank, &key, false, 20); + + let environments = ProgramRuntimeEnvironments::default(); + let expected = TransactionBatchProcessor::::load_program_from_bytes( + &mut LoadProgramMetrics::default(), + account_data.data(), + account_data.owner(), + account_data.data().len(), + 0, + environments.program_runtime_v1.clone(), + false, + ); + + assert_eq!(result, Arc::new(expected.unwrap())); + } + + #[test] + fn test_load_program_program_loader_v3() { + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let mut mock_bank = MockBankCallback::default(); + let batch_processor = TransactionBatchProcessor::::default(); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader_upgradeable::id()); + + let state = UpgradeableLoaderState::Program { + programdata_address: key2, + }; + account_data.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .insert(key1, account_data.clone()); + + let state = UpgradeableLoaderState::ProgramData { + slot: 0, + upgrade_authority_address: None, + }; + let mut account_data2 = AccountSharedData::default(); + account_data2.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .insert(key2, account_data2.clone()); + + // This should return an error + let result = batch_processor.load_program_with_pubkey(&mock_bank, &key1, false, 0); + let loaded_program = LoadedProgram::new_tombstone( + 0, + LoadedProgramType::FailedVerification( + batch_processor + .program_cache + .read() + .unwrap() + .get_environments_for_epoch(0) + .clone() + .program_runtime_v1, + ), + ); + assert_eq!(result, Arc::new(loaded_program)); + + let mut buffer = load_test_program(); + let mut header = bincode::serialize(&state).unwrap(); + let mut complement = vec![ + 0; + std::cmp::max( + 0, + UpgradeableLoaderState::size_of_programdata_metadata() - header.len() + ) + ]; + header.append(&mut complement); + header.append(&mut buffer); + account_data.set_data(header); + + mock_bank + .account_shared_data + .insert(key2, account_data.clone()); + + let result = batch_processor.load_program_with_pubkey(&mock_bank, &key1, false, 20); + + let data = account_data.data(); + account_data + .set_data(data[UpgradeableLoaderState::size_of_programdata_metadata()..].to_vec()); + + let environments = ProgramRuntimeEnvironments::default(); + let expected = TransactionBatchProcessor::::load_program_from_bytes( + &mut LoadProgramMetrics::default(), + account_data.data(), + account_data.owner(), + account_data.data().len(), + 0, + environments.program_runtime_v1.clone(), + false, + ); + assert_eq!(result, Arc::new(expected.unwrap())); + } + + #[test] + fn test_load_program_of_loader_v4() { + let key = Pubkey::new_unique(); + let mut mock_bank = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(loader_v4::id()); + let batch_processor = TransactionBatchProcessor::::default(); + + let loader_data = LoaderV4State { + slot: 0, + authority_address: Pubkey::new_unique(), + status: LoaderV4Status::Deployed, + }; + let encoded = unsafe { + std::mem::transmute::<&LoaderV4State, &[u8; LoaderV4State::program_data_offset()]>( + &loader_data, + ) + }; + account_data.set_data(encoded.to_vec()); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.load_program_with_pubkey(&mock_bank, &key, false, 0); + let loaded_program = LoadedProgram::new_tombstone( + 0, + LoadedProgramType::FailedVerification( + batch_processor + .program_cache + .read() + .unwrap() + .get_environments_for_epoch(0) + .clone() + .program_runtime_v1, + ), + ); + assert_eq!(result, Arc::new(loaded_program)); + + let mut header = account_data.data().to_vec(); + let mut complement = + vec![0; std::cmp::max(0, LoaderV4State::program_data_offset() - header.len())]; + header.append(&mut complement); + + let mut buffer = load_test_program(); + header.append(&mut buffer); + + account_data.set_data(header); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.load_program_with_pubkey(&mock_bank, &key, false, 20); + + let data = account_data.data()[LoaderV4State::program_data_offset()..].to_vec(); + account_data.set_data(data); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let environments = ProgramRuntimeEnvironments::default(); + let expected = TransactionBatchProcessor::::load_program_from_bytes( + &mut LoadProgramMetrics::default(), + account_data.data(), + account_data.owner(), + account_data.data().len(), + 0, + environments.program_runtime_v1.clone(), + false, + ); + assert_eq!(result, Arc::new(expected.unwrap())); + } + + #[test] + fn test_load_program_effective_slot() { + let key = Pubkey::new_unique(); + let mut mock_bank = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(loader_v4::id()); + let batch_processor = TransactionBatchProcessor::::default(); + + batch_processor + .program_cache + .write() + .unwrap() + .upcoming_environments = Some(ProgramRuntimeEnvironments::default()); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.load_program_with_pubkey(&mock_bank, &key, false, 20); + + let slot = batch_processor.epoch_schedule.get_first_slot_in_epoch(20); + assert_eq!(result.effective_slot, slot); + } + + #[test] + fn test_program_modification_slot_account_not_found() { + let batch_processor = TransactionBatchProcessor::::default(); + let mut mock_bank = MockBankCallback::default(); + let key = Pubkey::new_unique(); + + let result = batch_processor.program_modification_slot(&mock_bank, &key); + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader_upgradeable::id()); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.program_modification_slot(&mock_bank, &key); + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + + let state = UpgradeableLoaderState::Program { + programdata_address: Pubkey::new_unique(), + }; + account_data.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.program_modification_slot(&mock_bank, &key); + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + + account_data.set_owner(loader_v4::id()); + mock_bank + .account_shared_data + .insert(key, account_data.clone()); + + let result = batch_processor.program_modification_slot(&mock_bank, &key); + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + } + + #[test] + fn test_program_modification_slot_success() { + let batch_processor = TransactionBatchProcessor::::default(); + let mut mock_bank = MockBankCallback::default(); + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader_upgradeable::id()); + + let state = UpgradeableLoaderState::Program { + programdata_address: key2, + }; + account_data.set_data(bincode::serialize(&state).unwrap()); + mock_bank.account_shared_data.insert(key1, account_data); + + let state = UpgradeableLoaderState::ProgramData { + slot: 77, + upgrade_authority_address: None, + }; + let mut account_data = AccountSharedData::default(); + account_data.set_data(bincode::serialize(&state).unwrap()); + mock_bank.account_shared_data.insert(key2, account_data); + + let result = batch_processor.program_modification_slot(&mock_bank, &key1); + assert_eq!(result.unwrap(), 77); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(loader_v4::id()); + let state = LoaderV4State { + slot: 58, + authority_address: Pubkey::new_unique(), + status: LoaderV4Status::Deployed, + }; + + let encoded = unsafe { + std::mem::transmute::<&LoaderV4State, &[u8; LoaderV4State::program_data_offset()]>( + &state, + ) + }; + account_data.set_data(encoded.to_vec()); + mock_bank + .account_shared_data + .insert(key1, account_data.clone()); + + let result = batch_processor.program_modification_slot(&mock_bank, &key1); + assert_eq!(result.unwrap(), 58); + + account_data.set_owner(Pubkey::new_unique()); + mock_bank.account_shared_data.insert(key2, account_data); + + let result = batch_processor.program_modification_slot(&mock_bank, &key2); + assert_eq!(result.unwrap(), 0); + } + + #[test] + fn test_execute_loaded_transaction_recordings() { + // Setting all the arguments correctly is too burdensome for testing + // execute_loaded_transaction separately.This function will be tested in an integration + // test with load_and_execute_sanitized_transactions + let message = Message { + account_keys: vec![Pubkey::new_from_array([0; 32])], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let loaded_programs = LoadedProgramsForTxBatch::default(); + let mock_bank = MockBankCallback::default(); + let batch_processor = TransactionBatchProcessor::::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let mut loaded_transaction = LoadedTransaction { + accounts: vec![(Pubkey::new_unique(), AccountSharedData::default())], + program_indices: vec![vec![0]], + rent: 0, + rent_debits: RentDebits::default(), + }; + + let mut record_config = ExecutionRecordingConfig { + enable_cpi_recording: false, + enable_log_recording: true, + enable_return_data_recording: false, + }; + + let result = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + &mut loaded_transaction, + ComputeBudget::default(), + None, + record_config, + &mut ExecuteTimings::default(), + &mut TransactionErrorMetrics::default(), + None, + &loaded_programs, + ); + + let TransactionExecutionResult::Executed { + details: TransactionExecutionDetails { log_messages, .. }, + .. + } = result + else { + panic!("Unexpected result") + }; + assert!(log_messages.is_some()); + + let result = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + &mut loaded_transaction, + ComputeBudget::default(), + None, + record_config, + &mut ExecuteTimings::default(), + &mut TransactionErrorMetrics::default(), + Some(2), + &loaded_programs, + ); + + let TransactionExecutionResult::Executed { + details: + TransactionExecutionDetails { + log_messages, + inner_instructions, + .. + }, + .. + } = result + else { + panic!("Unexpected result") + }; + assert!(log_messages.is_some()); + assert!(inner_instructions.is_none()); + + record_config.enable_log_recording = false; + record_config.enable_cpi_recording = true; + + let result = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + &mut loaded_transaction, + ComputeBudget::default(), + None, + record_config, + &mut ExecuteTimings::default(), + &mut TransactionErrorMetrics::default(), + None, + &loaded_programs, + ); + + let TransactionExecutionResult::Executed { + details: + TransactionExecutionDetails { + log_messages, + inner_instructions, + .. + }, + .. + } = result + else { + panic!("Unexpected result") + }; + assert!(log_messages.is_none()); + assert!(inner_instructions.is_some()); + } + + #[test] + fn test_execute_loaded_transaction_error_metrics() { + // Setting all the arguments correctly is too burdensome for testing + // execute_loaded_transaction separately.This function will be tested in an integration + // test with load_and_execute_sanitized_transactions + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let message = Message { + account_keys: vec![key1, key2], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![2], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let loaded_programs = LoadedProgramsForTxBatch::default(); + let mock_bank = MockBankCallback::default(); + let batch_processor = TransactionBatchProcessor::::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader::id()); + let mut loaded_transaction = LoadedTransaction { + accounts: vec![ + (key1, AccountSharedData::default()), + (key2, AccountSharedData::default()), + ], + program_indices: vec![vec![0]], + rent: 0, + rent_debits: RentDebits::default(), + }; + + let record_config = ExecutionRecordingConfig::new_single_setting(false); + let mut error_metrics = TransactionErrorMetrics::new(); + + let _ = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + &mut loaded_transaction, + ComputeBudget::default(), + None, + record_config, + &mut ExecuteTimings::default(), + &mut error_metrics, + None, + &loaded_programs, + ); + + assert_eq!(error_metrics.instruction_error, 1); + } + + #[test] + fn test_replenish_program_cache() { + // Case 1 + let mut mock_bank = MockBankCallback::default(); + let batch_processor = TransactionBatchProcessor::::default(); + batch_processor.program_cache.write().unwrap().fork_graph = + Some(Arc::new(RwLock::new(TestForkGraph {}))); + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader::id()); + mock_bank.account_shared_data.insert(key2, account_data); + + let mut account_maps: HashMap = HashMap::new(); + account_maps.insert(key1, (&owner, 2)); + + account_maps.insert(key2, (&owner, 4)); + let result = batch_processor.replenish_program_cache(&mock_bank, &account_maps, false); + + let program1 = result.find(&key1).unwrap(); + assert!(matches!(program1.program, LoadedProgramType::Closed)); + assert!(!result.hit_max_limit); + let program2 = result.find(&key2).unwrap(); + assert!(matches!( + program2.program, + LoadedProgramType::FailedVerification(_) + )); + + // Case 2 + let result = batch_processor.replenish_program_cache(&mock_bank, &account_maps, true); + + let program1 = result.find(&key1).unwrap(); + assert!(matches!(program1.program, LoadedProgramType::Closed)); + assert!(!result.hit_max_limit); + let program2 = result.find(&key2).unwrap(); + assert!(matches!( + program2.program, + LoadedProgramType::FailedVerification(_) + )); + } + + #[test] + fn test_filter_executable_program_accounts() { + let mut mock_bank = MockBankCallback::default(); + let key1 = Pubkey::new_unique(); + let owner1 = Pubkey::new_unique(); + + let mut data = AccountSharedData::default(); + data.set_owner(owner1); + data.set_lamports(93); + mock_bank.account_shared_data.insert(key1, data); + + let message = Message { + account_keys: vec![key1], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + + let sanitized_transaction_1 = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let key2 = Pubkey::new_unique(); + let owner2 = Pubkey::new_unique(); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(owner2); + account_data.set_lamports(90); + mock_bank.account_shared_data.insert(key2, account_data); + + let message = Message { + account_keys: vec![key1, key2], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + + let sanitized_transaction_2 = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let transactions = vec![ + sanitized_transaction_1.clone(), + sanitized_transaction_2.clone(), + sanitized_transaction_2, + sanitized_transaction_1, + ]; + let mut lock_results = vec![ + (Ok(()), None, Some(25)), + (Ok(()), None, Some(25)), + (Ok(()), None, None), + (Err(TransactionError::ProgramAccountNotFound), None, None), + ]; + let owners = vec![owner1, owner2]; + + let result = TransactionBatchProcessor::::filter_executable_program_accounts( + &mock_bank, + &transactions, + lock_results.as_mut_slice(), + &owners, + ); + + assert_eq!( + lock_results[2], + (Err(TransactionError::BlockhashNotFound), None, None) + ); + assert_eq!(result.len(), 2); + assert_eq!(result[&key1], (&owner1, 2)); + assert_eq!(result[&key2], (&owner2, 1)); + } + + #[test] + fn test_filter_executable_program_accounts_no_errors() { + let keypair1 = Keypair::new(); + let keypair2 = Keypair::new(); + + let non_program_pubkey1 = Pubkey::new_unique(); + let non_program_pubkey2 = Pubkey::new_unique(); + let program1_pubkey = Pubkey::new_unique(); + let program2_pubkey = Pubkey::new_unique(); + let account1_pubkey = Pubkey::new_unique(); + let account2_pubkey = Pubkey::new_unique(); + let account3_pubkey = Pubkey::new_unique(); + let account4_pubkey = Pubkey::new_unique(); + + let account5_pubkey = Pubkey::new_unique(); + + let mut bank = MockBankCallback::default(); + bank.account_shared_data.insert( + non_program_pubkey1, + AccountSharedData::new(1, 10, &account5_pubkey), + ); + bank.account_shared_data.insert( + non_program_pubkey2, + AccountSharedData::new(1, 10, &account5_pubkey), + ); + bank.account_shared_data.insert( + program1_pubkey, + AccountSharedData::new(40, 1, &account5_pubkey), + ); + bank.account_shared_data.insert( + program2_pubkey, + AccountSharedData::new(40, 1, &account5_pubkey), + ); + bank.account_shared_data.insert( + account1_pubkey, + AccountSharedData::new(1, 10, &non_program_pubkey1), + ); + bank.account_shared_data.insert( + account2_pubkey, + AccountSharedData::new(1, 10, &non_program_pubkey2), + ); + bank.account_shared_data.insert( + account3_pubkey, + AccountSharedData::new(40, 1, &program1_pubkey), + ); + bank.account_shared_data.insert( + account4_pubkey, + AccountSharedData::new(40, 1, &program2_pubkey), + ); + + let tx1 = Transaction::new_with_compiled_instructions( + &[&keypair1], + &[non_program_pubkey1], + Hash::new_unique(), + vec![account1_pubkey, account2_pubkey, account3_pubkey], + vec![CompiledInstruction::new(1, &(), vec![0])], + ); + let sanitized_tx1 = SanitizedTransaction::from_transaction_for_tests(tx1); + + let tx2 = Transaction::new_with_compiled_instructions( + &[&keypair2], + &[non_program_pubkey2], + Hash::new_unique(), + vec![account4_pubkey, account3_pubkey, account2_pubkey], + vec![CompiledInstruction::new(1, &(), vec![0])], + ); + let sanitized_tx2 = SanitizedTransaction::from_transaction_for_tests(tx2); + + let owners = &[program1_pubkey, program2_pubkey]; + let programs = + TransactionBatchProcessor::::filter_executable_program_accounts( + &bank, + &[sanitized_tx1, sanitized_tx2], + &mut [(Ok(()), None, Some(0)), (Ok(()), None, Some(0))], + owners, + ); + + // The result should contain only account3_pubkey, and account4_pubkey as the program accounts + assert_eq!(programs.len(), 2); + assert_eq!( + programs + .get(&account3_pubkey) + .expect("failed to find the program account"), + &(&program1_pubkey, 2) + ); + assert_eq!( + programs + .get(&account4_pubkey) + .expect("failed to find the program account"), + &(&program2_pubkey, 1) + ); + } + + #[test] + fn test_filter_executable_program_accounts_invalid_blockhash() { + let keypair1 = Keypair::new(); + let keypair2 = Keypair::new(); + + let non_program_pubkey1 = Pubkey::new_unique(); + let non_program_pubkey2 = Pubkey::new_unique(); + let program1_pubkey = Pubkey::new_unique(); + let program2_pubkey = Pubkey::new_unique(); + let account1_pubkey = Pubkey::new_unique(); + let account2_pubkey = Pubkey::new_unique(); + let account3_pubkey = Pubkey::new_unique(); + let account4_pubkey = Pubkey::new_unique(); + + let account5_pubkey = Pubkey::new_unique(); + + let mut bank = MockBankCallback::default(); + bank.account_shared_data.insert( + non_program_pubkey1, + AccountSharedData::new(1, 10, &account5_pubkey), + ); + bank.account_shared_data.insert( + non_program_pubkey2, + AccountSharedData::new(1, 10, &account5_pubkey), + ); + bank.account_shared_data.insert( + program1_pubkey, + AccountSharedData::new(40, 1, &account5_pubkey), + ); + bank.account_shared_data.insert( + program2_pubkey, + AccountSharedData::new(40, 1, &account5_pubkey), + ); + bank.account_shared_data.insert( + account1_pubkey, + AccountSharedData::new(1, 10, &non_program_pubkey1), + ); + bank.account_shared_data.insert( + account2_pubkey, + AccountSharedData::new(1, 10, &non_program_pubkey2), + ); + bank.account_shared_data.insert( + account3_pubkey, + AccountSharedData::new(40, 1, &program1_pubkey), + ); + bank.account_shared_data.insert( + account4_pubkey, + AccountSharedData::new(40, 1, &program2_pubkey), + ); + + let tx1 = Transaction::new_with_compiled_instructions( + &[&keypair1], + &[non_program_pubkey1], + Hash::new_unique(), + vec![account1_pubkey, account2_pubkey, account3_pubkey], + vec![CompiledInstruction::new(1, &(), vec![0])], + ); + let sanitized_tx1 = SanitizedTransaction::from_transaction_for_tests(tx1); + + let tx2 = Transaction::new_with_compiled_instructions( + &[&keypair2], + &[non_program_pubkey2], + Hash::new_unique(), + vec![account4_pubkey, account3_pubkey, account2_pubkey], + vec![CompiledInstruction::new(1, &(), vec![0])], + ); + // Let's not register blockhash from tx2. This should cause the tx2 to fail + let sanitized_tx2 = SanitizedTransaction::from_transaction_for_tests(tx2); + + let owners = &[program1_pubkey, program2_pubkey]; + let mut lock_results = vec![(Ok(()), None, Some(0)), (Ok(()), None, None)]; + let programs = + TransactionBatchProcessor::::filter_executable_program_accounts( + &bank, + &[sanitized_tx1, sanitized_tx2], + &mut lock_results, + owners, + ); + + // The result should contain only account3_pubkey as the program accounts + assert_eq!(programs.len(), 1); + assert_eq!( + programs + .get(&account3_pubkey) + .expect("failed to find the program account"), + &(&program1_pubkey, 1) + ); + assert_eq!(lock_results[1].0, Err(TransactionError::BlockhashNotFound)); + } +}