diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index e59b9d3fe60..4b9374db8b2 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1126,6 +1126,18 @@ pub mod enforce_fixed_fec_set { solana_pubkey::declare_id!("fixfecLZYMfkGzwq6NJA11Yw6KYztzXiK9QcL3K78in"); } +pub mod replace_spl_token_with_p_token { + use super::Pubkey; + + solana_pubkey::declare_id!("ptokSWRqZz5u2xdqMdstkMKpFurauUpVen7TZXgDpkQ"); + + pub const SPL_TOKEN_PROGRAM_ID: Pubkey = + Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + + pub const PTOKEN_PROGRAM_BUFFER: Pubkey = + Pubkey::from_str_const("ptokNfvuU7terQ2r2452RzVXB3o4GT33yPWo1fUkkZ2"); +} + pub static FEATURE_NAMES: LazyLock> = LazyLock::new(|| { [ (secp256k1_program_enabled::id(), "secp256k1 program"), @@ -1368,6 +1380,7 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n (raise_account_cu_limit::id(), "SIMD-0306: Raise account CU limit to 40% max"), (raise_cpi_nesting_limit_to_8::id(), "SIMD-0296: Raise CPI nesting limit from 4 to 8"), (enforce_fixed_fec_set::id(), "SIMD-0317: Enforce 32 data + 32 coding shreds"), + (replace_spl_token_with_p_token::id(), "SIMD-0266: Efficient Token program"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index ee8ab5c5527..03706dcd5a1 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -5324,6 +5324,19 @@ impl Bank { if new_feature_activations.contains(&feature_set::raise_account_cu_limit::id()) { self.apply_simd_0306_cost_tracker_changes(); } + + if new_feature_activations.contains(&feature_set::replace_spl_token_with_p_token::id()) { + if let Err(e) = self.upgrade_loader_v2_owned_program( + &feature_set::replace_spl_token_with_p_token::SPL_TOKEN_PROGRAM_ID, + &feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER, + "replace_spl_token_with_p_token", + ) { + warn!( + "Failed to replace SPL Token with p-token buffer '{}': {e}", + feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER, + ); + } + } } fn adjust_sysvar_balance_for_rent(&self, account: &mut AccountSharedData) { diff --git a/runtime/src/bank/builtins/core_bpf_migration/mod.rs b/runtime/src/bank/builtins/core_bpf_migration/mod.rs index 8d710202c4c..fee41441f16 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/mod.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/mod.rs @@ -1,10 +1,11 @@ pub(crate) mod error; mod source_buffer; +mod target_bpf_v2; mod target_builtin; mod target_core_bpf; use { - crate::bank::Bank, + crate::bank::{builtins::core_bpf_migration::target_bpf_v2::TargetBpfV2, Bank}, error::CoreBpfMigrationError, num_traits::{CheckedAdd, CheckedSub}, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, @@ -15,15 +16,18 @@ use { solana_loader_v3_interface::state::UpgradeableLoaderState, solana_program_runtime::{ invoke_context::{EnvironmentConfig, InvokeContext}, - loaded_programs::ProgramCacheForTxBatch, + loaded_programs::{LoadProgramMetrics, ProgramCacheEntry, ProgramCacheForTxBatch}, sysvar_cache::SysvarCache, }, solana_pubkey::Pubkey, - solana_sdk_ids::bpf_loader_upgradeable, + solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable}, solana_svm_callback::InvokeContextCallback, solana_transaction_context::TransactionContext, source_buffer::SourceBuffer, - std::{cmp::Ordering, sync::atomic::Ordering::Relaxed}, + std::{ + cmp::Ordering, + sync::{atomic::Ordering::Relaxed, Arc}, + }, target_builtin::TargetBuiltin, target_core_bpf::TargetCoreBpf, }; @@ -115,6 +119,31 @@ impl Bank { } } + /// Create an `AccountSharedData` with data initialized to the source buffer + /// account's ELF and owned by the BPF Loader V2 program. + fn new_loader_v2_target_program_account( + &self, + source: &SourceBuffer, + ) -> Result { + let buffer_metadata_size = UpgradeableLoaderState::size_of_buffer_metadata(); + if let UpgradeableLoaderState::Buffer { .. } = + bincode::deserialize(&source.buffer_account.data()[..buffer_metadata_size])? + { + let elf = &source.buffer_account.data()[buffer_metadata_size..]; + let lamports = self.get_minimum_balance_for_rent_exemption(elf.len()); + + let mut account = AccountSharedData::new(lamports, elf.len(), &bpf_loader::id()); + account.set_executable(true); + account.data_as_mut_slice().copy_from_slice(elf); + + Ok(account) + } else { + Err(CoreBpfMigrationError::InvalidBufferAccount( + source.buffer_address, + )) + } + } + /// In order to properly update a newly migrated or upgraded Core BPF /// program in the program cache, the runtime must directly invoke the BPF /// Upgradeable Loader's deployment functionality for validating the ELF @@ -218,6 +247,104 @@ impl Bank { Ok(()) } + /// Replace a Loader v2 owned program data. + /// + /// Note that the `programdata` corresponds to a Loader v3 buffer, which + /// has a metadata "header" ahead the ELF binary. + fn replace_loader_v2_owned_program_data( + &self, + program_id: &Pubkey, + programdata: &[u8], + ) -> Result<(), InstructionError> { + // Set up the two `LoadedProgramsForTxBatch` instances, as if + // processing a new transaction batch. + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new_from_cache( + self.slot, + self.epoch, + &self + .transaction_processor + .global_program_cache + .read() + .unwrap(), + ); + + // Configure a dummy `InvokeContext` from the runtime's current + // environment, as well as the two `ProgramCacheForTxBatch` + // instances configured above. + { + let compute_budget = self + .compute_budget() + .unwrap_or(ComputeBudget::new_with_defaults( + /* simd_0296_active */ false, + )); + let mut sysvar_cache = SysvarCache::default(); + sysvar_cache.fill_missing_entries(|pubkey, set_sysvar| { + if let Some(account) = self.get_account(pubkey) { + set_sysvar(account.data()); + } + }); + + let mut dummy_transaction_context = TransactionContext::new( + vec![], + self.rent_collector.rent.clone(), + compute_budget.max_instruction_stack_depth, + compute_budget.max_instruction_trace_length, + ); + + struct MockCallback {} + impl InvokeContextCallback for MockCallback {} + let feature_set = self.feature_set.runtime_features(); + let dummy_invoke_context = InvokeContext::new( + &mut dummy_transaction_context, + &mut program_cache_for_tx_batch, + EnvironmentConfig::new( + Hash::default(), + 0, + &MockCallback {}, + &feature_set, + &sysvar_cache, + ), + None, + compute_budget.to_budget(), + compute_budget.to_cost(), + ); + + let environments = dummy_invoke_context + .get_environments_for_slot(self.slot) + .map_err(|_err| { + // This will never fail since the epoch schedule is already configured. + InstructionError::ProgramEnvironmentSetupFailure + })?; + + // We need to pass one instance of `LoadProgramMetrics` to create a new + // cache entry. + let mut metrics = LoadProgramMetrics::default(); + + let updated = ProgramCacheEntry::new( + &bpf_loader::id(), + environments.program_runtime_v1.clone(), + self.slot, + self.slot, + programdata, + programdata.len(), + &mut metrics, + ) + .map_err(|_err| InstructionError::ProgramEnvironmentSetupFailure)?; + + program_cache_for_tx_batch.store_modified_entry(*program_id, Arc::new(updated)); + } + + // Update the program cache by merging with `programs_modified`, which + // was modified to replace the program data. + self.transaction_processor + .global_program_cache + .write() + .unwrap() + .merge(&program_cache_for_tx_batch.drain_modified_entries()); + + Ok(()) + } + pub(crate) fn migrate_builtin_to_core_bpf( &mut self, builtin_program_id: &Pubkey, @@ -373,6 +500,76 @@ impl Bank { Ok(()) } + /// Upgrade a Loader v2 owned BPF program. + /// + /// To use this function, add a feature-gated callsite to bank's + /// `apply_feature_activations` function, similar to below. + /// + /// ```ignore + /// if new_feature_activations.contains(&agave_feature_set::test_upgrade_program::id()) { + /// self.upgrade_loader_v2_owned_program( + /// &bpf_loader_v2_program_address, + /// &source_buffer_address, + /// "test_upgrade_loader_v2_owned_program", + /// ); + /// } + /// ``` + /// The `source_buffer_address` must point to a Loader v3 buffer account + /// (state equal to [`UpgradeableLoaderState::Buffer`]). + #[allow(dead_code)] // Only used when an upgrade is configured. + pub(crate) fn upgrade_loader_v2_owned_program( + &mut self, + loader_v2_bpf_program_address: &Pubkey, + source_buffer_address: &Pubkey, + datapoint_name: &'static str, + ) -> Result<(), CoreBpfMigrationError> { + datapoint_info!(datapoint_name, ("slot", self.slot, i64)); + + let target = TargetBpfV2::new_checked(self, loader_v2_bpf_program_address)?; + let source = SourceBuffer::new_checked(self, source_buffer_address)?; + + // Attempt serialization first before modifying the bank. + let new_target_program_account = self.new_loader_v2_target_program_account(&source)?; + + // Gather old and new account data sizes, for updating the bank's + // accounts data size delta off-chain. + // The old data size is the total size of all original accounts + // involved. + // The new data size is the total size of all the new program accounts. + let old_data_size = checked_add( + target.program_account.data().len(), + source.buffer_account.data().len(), + )?; + // Size of the buffer is always greater than the metadata size. + let new_data_size = new_target_program_account.data().len(); + + // Replaces the program account data. + self.replace_loader_v2_owned_program_data( + &target.program_address, + new_target_program_account.data(), + )?; + + // Calculate the lamports to burn. + // The target program account will change size, so burn its lamports. + // The source buffer account will be cleared, so burn its lamports. + // The new target program account will need to be funded. + let lamports_to_burn = checked_add( + target.program_account.lamports(), + source.buffer_account.lamports(), + )?; + let lamports_to_fund = new_target_program_account.lamports(); + self.update_captalization(lamports_to_burn, lamports_to_fund)?; + + // Store the new program accounts and clear the source buffer account. + self.store_account(&target.program_address, &new_target_program_account); + self.store_account(&source.buffer_address, &AccountSharedData::default()); + + // Update the account data size delta. + self.calculate_and_update_accounts_data_size_delta_off_chain(old_data_size, new_data_size); + + Ok(()) + } + fn update_captalization( &mut self, lamports_to_burn: u64, @@ -420,12 +617,13 @@ pub(crate) mod tests { solana_epoch_schedule::EpochSchedule, solana_feature_gate_interface::{self as feature, Feature}, solana_instruction::{AccountMeta, Instruction}, + solana_keypair::Keypair, solana_loader_v3_interface::{get_program_data_address, state::UpgradeableLoaderState}, solana_message::Message, solana_native_token::LAMPORTS_PER_SOL, solana_program_runtime::loaded_programs::{ProgramCacheEntry, ProgramCacheEntryType}, solana_pubkey::Pubkey, - solana_sdk_ids::{bpf_loader_upgradeable, native_loader}, + solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable, native_loader}, solana_signer::Signer, solana_transaction::Transaction, std::{fs::File, io::Read, sync::Arc}, @@ -1162,54 +1360,17 @@ pub(crate) mod tests { } } - // This test can't be used to the `compute_budget` program, unless a valid - // `compute_budget` program is provided as the replacement (source). - // See program_runtime::compute_budget_processor::process_compute_budget_instructions`.` - // It also can't test the `bpf_loader_upgradeable` program, as it's used in - // the SVM's loader to invoke programs. - // See `solana_svm::account_loader::load_transaction_accounts`. - #[test_case(TestPrototype::Builtin(&BUILTINS[0]); "system")] - #[test_case(TestPrototype::Builtin(&BUILTINS[1]); "vote")] - #[test_case(TestPrototype::Builtin(&BUILTINS[2]); "bpf_loader_deprecated")] - #[test_case(TestPrototype::Builtin(&BUILTINS[3]); "bpf_loader")] - fn test_migrate_builtin_e2e(prototype: TestPrototype) { - let (mut genesis_config, mint_keypair) = - create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - let slots_per_epoch = 32; - genesis_config.epoch_schedule = - EpochSchedule::custom(slots_per_epoch, slots_per_epoch, false); - - let mut root_bank = Bank::new_for_tests(&genesis_config); - - // Set up the CPI mockup to test CPI'ing to the migrated program. - let cpi_program_id = Pubkey::new_unique(); - let cpi_program_name = "mock_cpi_program"; - root_bank.transaction_processor.add_builtin( - &root_bank, - cpi_program_id, - cpi_program_name, - ProgramCacheEntry::new_builtin(0, cpi_program_name.len(), cpi_mockup::Entrypoint::vm), - ); - - let (builtin_id, config) = prototype.deconstruct(); - let feature_id = &config.feature_id; - let source_buffer_address = &config.source_buffer_address; - let upgrade_authority_address = config.upgrade_authority_address; - - // Add the feature to the bank's inactive feature set. - // Note this will add the feature ID if it doesn't exist. - let mut feature_set = FeatureSet::all_enabled(); - feature_set.deactivate(feature_id); - root_bank.feature_set = Arc::new(feature_set); - - // Initialize the source buffer account. - let test_context = TestContext::new( - &root_bank, - builtin_id, - source_buffer_address, - upgrade_authority_address, - ); - + /// Activate a feature and run checks on the test context. + fn activate_feature_and_run_checks( + root_bank: Bank, + test_context: &TestContext, + program_id: &Pubkey, + feature_id: &Pubkey, + source_buffer_address: &Pubkey, + mint_keypair: &Keypair, + slots_per_epoch: u64, + cpi_program_id: &Pubkey, + ) { let (bank, bank_forks) = root_bank.wrap_with_bank_forks_for_tests(); // Advance to the next epoch without activating the feature. @@ -1248,32 +1409,32 @@ pub(crate) mod tests { assert!(bank.feature_set.is_active(feature_id)); test_context.run_program_checks(&bank, migration_slot); - // Advance one slot so that the new BPF builtin program becomes + // Advance one slot so that the new BPF loader v3 program becomes // effective in the program cache. goto_end_of_slot(bank.clone()); let next_slot = bank.slot() + 1; let bank = new_bank_from_parent_with_bank_forks(&bank_forks, bank, &Pubkey::default(), next_slot); - // Successfully invoke the new BPF builtin program. + // Successfully invoke the new BPF loader v3 program. bank.process_transaction(&Transaction::new( &vec![&mint_keypair], Message::new( - &[Instruction::new_with_bytes(*builtin_id, &[], Vec::new())], + &[Instruction::new_with_bytes(*program_id, &[], Vec::new())], Some(&mint_keypair.pubkey()), ), bank.last_blockhash(), )) .unwrap(); - // Successfully invoke the new BPF builtin program via CPI. + // Successfully invoke the new BPF loader v3 program via CPI. bank.process_transaction(&Transaction::new( &vec![&mint_keypair], Message::new( &[Instruction::new_with_bytes( - cpi_program_id, + *cpi_program_id, &[], - vec![AccountMeta::new_readonly(*builtin_id, false)], + vec![AccountMeta::new_readonly(*program_id, false)], )], Some(&mint_keypair.pubkey()), ), @@ -1295,25 +1456,25 @@ pub(crate) mod tests { assert!(bank.feature_set.is_active(feature_id)); test_context.run_program_checks(&bank, migration_slot); - // Again, successfully invoke the new BPF builtin program. + // Again, successfully invoke the new BPF loader v3 program. bank.process_transaction(&Transaction::new( &vec![&mint_keypair], Message::new( - &[Instruction::new_with_bytes(*builtin_id, &[], Vec::new())], + &[Instruction::new_with_bytes(*program_id, &[], Vec::new())], Some(&mint_keypair.pubkey()), ), bank.last_blockhash(), )) .unwrap(); - // Again, successfully invoke the new BPF builtin program via CPI. + // Again, successfully invoke the new BPF loader v3 program via CPI. bank.process_transaction(&Transaction::new( &vec![&mint_keypair], Message::new( &[Instruction::new_with_bytes( - cpi_program_id, + *cpi_program_id, &[], - vec![AccountMeta::new_readonly(*builtin_id, false)], + vec![AccountMeta::new_readonly(*program_id, false)], )], Some(&mint_keypair.pubkey()), ), @@ -1322,6 +1483,67 @@ pub(crate) mod tests { .unwrap(); } + // This test can't be used to the `compute_budget` program, unless a valid + // `compute_budget` program is provided as the replacement (source). + // See program_runtime::compute_budget_processor::process_compute_budget_instructions`.` + // It also can't test the `bpf_loader_upgradeable` program, as it's used in + // the SVM's loader to invoke programs. + // See `solana_svm::account_loader::load_transaction_accounts`. + #[test_case(TestPrototype::Builtin(&BUILTINS[0]); "system")] + #[test_case(TestPrototype::Builtin(&BUILTINS[1]); "vote")] + #[test_case(TestPrototype::Builtin(&BUILTINS[2]); "bpf_loader_deprecated")] + #[test_case(TestPrototype::Builtin(&BUILTINS[3]); "bpf_loader")] + fn test_migrate_builtin_e2e(prototype: TestPrototype) { + let (mut genesis_config, mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + let slots_per_epoch = 32; + genesis_config.epoch_schedule = + EpochSchedule::custom(slots_per_epoch, slots_per_epoch, false); + + let mut root_bank = Bank::new_for_tests(&genesis_config); + + // Set up the CPI mockup to test CPI'ing to the migrated program. + let cpi_program_id = Pubkey::new_unique(); + let cpi_program_name = "mock_cpi_program"; + root_bank.transaction_processor.add_builtin( + &root_bank, + cpi_program_id, + cpi_program_name, + ProgramCacheEntry::new_builtin(0, cpi_program_name.len(), cpi_mockup::Entrypoint::vm), + ); + + let (builtin_id, config) = prototype.deconstruct(); + let feature_id = &config.feature_id; + let source_buffer_address = &config.source_buffer_address; + let upgrade_authority_address = config.upgrade_authority_address; + + // Add the feature to the bank's inactive feature set. + // Note this will add the feature ID if it doesn't exist. + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(feature_id); + root_bank.feature_set = Arc::new(feature_set); + + // Initialize the source buffer account. + let test_context = TestContext::new( + &root_bank, + builtin_id, + source_buffer_address, + upgrade_authority_address, + ); + + // Activate the feature and run the migration checks. + activate_feature_and_run_checks( + root_bank, + &test_context, + builtin_id, + feature_id, + source_buffer_address, + &mint_keypair, + slots_per_epoch, + &cpi_program_id, + ); + } + // Simulate a failure to migrate the program. // Here we want to see that the bank handles the failure gracefully and // advances to the next epoch without issue. @@ -1625,4 +1847,285 @@ pub(crate) mod tests { check_builtin_is_bpf(&bank); } + + #[test] + fn test_upgrade_loader_v2_owned_program() { + let mut bank = create_simple_test_bank(0); + + let bpf_loader_v2_program_address = Pubkey::new_unique(); + let source_buffer_address = Pubkey::new_unique(); + + { + let program_account = { + let elf = [4u8; 200]; // Mock ELF to start. + let space = elf.len(); + let lamports = bank.get_minimum_balance_for_rent_exemption(space); + let owner = &bpf_loader::id(); + + let mut account = AccountSharedData::new(lamports, space, owner); + account.set_executable(true); + account.data_as_mut_slice().copy_from_slice(&elf); + bank.store_account_and_update_capitalization( + &bpf_loader_v2_program_address, + &account, + ); + account + }; + + assert_eq!( + &bank.get_account(&bpf_loader_v2_program_address).unwrap(), + &program_account + ); + }; + + let test_context = TestContext::new( + &bank, + &bpf_loader_v2_program_address, + &source_buffer_address, + None, + ); + let TestContext { + source_buffer_address, + .. + } = test_context; + + let ( + expected_post_upgrade_capitalization, + expected_post_upgrade_accounts_data_size_delta_off_chain, + ) = test_context + .calculate_post_migration_capitalization_and_accounts_data_size_delta_off_chain(&bank); + + // Perform the upgrade. + let upgrade_slot = bank.slot(); + bank.upgrade_loader_v2_owned_program( + &bpf_loader_v2_program_address, + &source_buffer_address, + "test_upgrade_loader_v2_owned_program", + ) + .unwrap(); + + // Run the post-upgrade program checks. + test_context.run_program_checks(&bank, upgrade_slot); + + // Check the bank's capitalization. + assert_eq!(bank.capitalization(), expected_post_upgrade_capitalization); + + // Check the bank's accounts data size delta off-chain. + assert_eq!( + bank.accounts_data_size_delta_off_chain.load(Relaxed), + expected_post_upgrade_accounts_data_size_delta_off_chain + ); + + // Check the migrated program account is now owned by the upgradeable loader. + let migrated_program_account = bank.get_account(&bpf_loader_v2_program_address).unwrap(); + assert_eq!( + migrated_program_account.owner(), + &bpf_loader_upgradeable::id() + ); + } + + #[test] + fn test_upgrade_loader_v2_owned_program_failure() { + let mut bank = create_simple_test_bank(0); + + let bpf_loader_v2_program_address = Pubkey::new_unique(); + let source_buffer_address = Pubkey::new_unique(); + + { + let program_account = { + let elf = [4u8; 200]; // Mock ELF to start. + let space = elf.len(); + let lamports = bank.get_minimum_balance_for_rent_exemption(space); + let owner = &bpf_loader::id(); + + let mut account = AccountSharedData::new(lamports, space, owner); + account.set_executable(true); + account.data_as_mut_slice().copy_from_slice(&elf); + bank.store_account_and_update_capitalization( + &bpf_loader_v2_program_address, + &account, + ); + account + }; + + assert_eq!( + &bank.get_account(&bpf_loader_v2_program_address).unwrap(), + &program_account + ); + }; + + // Set up the source buffer with a valid authority, but the migration + // config will define the upgrade authority to be `None`. + { + let elf = test_elf(); + let buffer_metadata_size = UpgradeableLoaderState::size_of_buffer_metadata(); + let space = buffer_metadata_size + elf.len(); + let lamports = bank.get_minimum_balance_for_rent_exemption(space); + let owner = &bpf_loader_upgradeable::id(); + + let buffer_metadata = UpgradeableLoaderState::Program { + programdata_address: Pubkey::new_unique(), + }; + + let mut account = + AccountSharedData::new_data_with_space(lamports, &buffer_metadata, space, owner) + .unwrap(); + account.data_as_mut_slice()[buffer_metadata_size..].copy_from_slice(&elf); + + bank.store_account_and_update_capitalization(&source_buffer_address, &account); + } + + // Try to perform the upgrade. + assert_matches!( + bank.upgrade_loader_v2_owned_program( + &bpf_loader_v2_program_address, + &source_buffer_address, + "test_upgrade_loader_v2_owned_program", + ) + .unwrap_err(), + CoreBpfMigrationError::InvalidBufferAccount(_) + ) + } + + /// Mock BPF loader v2 program for testing. + fn mock_bpf_loader_v2_program(bank: &Bank) -> AccountSharedData { + let elf = [4u8; 200]; // Mock ELF to start. + let space = elf.len(); + let lamports = bank.get_minimum_balance_for_rent_exemption(space); + let owner = &bpf_loader::id(); + + let mut account = AccountSharedData::new(lamports, space, owner); + account.set_executable(true); + account.data_as_mut_slice().copy_from_slice(&elf); + + account + } + + #[test] + fn test_replace_spl_token_with_p_token_e2e() { + let (mut genesis_config, mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + let slots_per_epoch = 32; + genesis_config.epoch_schedule = + EpochSchedule::custom(slots_per_epoch, slots_per_epoch, false); + + let mut root_bank = Bank::new_for_tests(&genesis_config); + + let feature_id = agave_feature_set::replace_spl_token_with_p_token::id(); + let program_id = agave_feature_set::replace_spl_token_with_p_token::SPL_TOKEN_PROGRAM_ID; + let source_buffer_address = + agave_feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER; + + // Set up a mock BPF loader v2 program. + { + let program_account = mock_bpf_loader_v2_program(&root_bank); + root_bank.store_account_and_update_capitalization(&program_id, &program_account); + assert_eq!( + &root_bank.get_account(&program_id).unwrap(), + &program_account + ); + }; + + // Set up the CPI mockup to test CPI'ing to the migrated program. + let cpi_program_id = Pubkey::new_unique(); + let cpi_program_name = "mock_cpi_program"; + root_bank.transaction_processor.add_builtin( + &root_bank, + cpi_program_id, + cpi_program_name, + ProgramCacheEntry::new_builtin(0, cpi_program_name.len(), cpi_mockup::Entrypoint::vm), + ); + + // Add the feature to the bank's inactive feature set. + // Note this will add the feature ID if it doesn't exist. + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&feature_id); + root_bank.feature_set = Arc::new(feature_set); + + // Initialize the source buffer account. + let test_context = TestContext::new(&root_bank, &program_id, &source_buffer_address, None); + + // Activate the feature and run the necessary checks. + activate_feature_and_run_checks( + root_bank, + &test_context, + &program_id, + &feature_id, + &source_buffer_address, + &mint_keypair, + slots_per_epoch, + &cpi_program_id, + ); + } + + // Simulate a failure to migrate the program. + // Here we want to see that the bank handles the failure gracefully and + // advances to the next epoch without issue. + #[test] + fn test_test_replace_spl_token_with_p_token_e2e_failure() { + let (genesis_config, _mint_keypair) = create_genesis_config(0); + let mut root_bank = Bank::new_for_tests(&genesis_config); + + let feature_id = &agave_feature_set::replace_spl_token_with_p_token::id(); + let program_id = &agave_feature_set::replace_spl_token_with_p_token::SPL_TOKEN_PROGRAM_ID; + let source_buffer_address = + &agave_feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER; + + // Set up a mock BPF loader v2 program. + { + let program_account = mock_bpf_loader_v2_program(&root_bank); + root_bank.store_account_and_update_capitalization(program_id, &program_account); + assert_eq!( + &root_bank.get_account(program_id).unwrap(), + &program_account + ); + }; + + // Add the feature to the bank's inactive feature set. + let mut feature_set = FeatureSet::all_enabled(); + feature_set.inactive_mut().insert(*feature_id); + root_bank.feature_set = Arc::new(feature_set); + + // Initialize the source buffer account. + let _test_context = TestContext::new(&root_bank, program_id, source_buffer_address, None); + + let (bank, bank_forks) = root_bank.wrap_with_bank_forks_for_tests(); + + // Intentionally nuke the source buffer account to force the migration + // to fail. + bank.store_account_and_update_capitalization( + source_buffer_address, + &AccountSharedData::default(), + ); + + // Activate the feature. + bank.store_account_and_update_capitalization( + feature_id, + &feature::create_account(&Feature::default(), 42), + ); + + // Advance the bank to cross the epoch boundary and activate the + // feature. + goto_end_of_slot(bank.clone()); + let bank = new_bank_from_parent_with_bank_forks(&bank_forks, bank, &Pubkey::default(), 33); + + // Assert the feature _was_ activated but the program was not migrated. + assert!(bank.feature_set.is_active(feature_id)); + assert_eq!( + bank.get_account(program_id).unwrap().owner(), + &bpf_loader::id() + ); + + // Simulate crossing an epoch boundary again. + goto_end_of_slot(bank.clone()); + let bank = new_bank_from_parent_with_bank_forks(&bank_forks, bank, &Pubkey::default(), 96); + + // Again, assert the feature is still active and the program still was + // not migrated. + assert!(bank.feature_set.is_active(feature_id)); + assert_eq!( + bank.get_account(program_id).unwrap().owner(), + &bpf_loader::id() + ); + } } diff --git a/runtime/src/bank/builtins/core_bpf_migration/target_bpf_v2.rs b/runtime/src/bank/builtins/core_bpf_migration/target_bpf_v2.rs new file mode 100644 index 00000000000..56ec637e6eb --- /dev/null +++ b/runtime/src/bank/builtins/core_bpf_migration/target_bpf_v2.rs @@ -0,0 +1,128 @@ +#![allow(dead_code)] +use { + super::error::CoreBpfMigrationError, + crate::bank::Bank, + solana_account::{AccountSharedData, ReadableAccount}, + solana_loader_v3_interface::get_program_data_address, + solana_pubkey::Pubkey, + solana_sdk_ids::bpf_loader, +}; + +/// The account details of a Loader v2 BPF program slated to be upgraded. +#[derive(Debug)] +pub(crate) struct TargetBpfV2 { + pub program_address: Pubkey, + pub program_account: AccountSharedData, + pub program_data_address: Pubkey, +} + +impl TargetBpfV2 { + /// Collects the details of a Loader v2 BPF program and verifies it is properly + /// configured. + /// + /// The program account should exist and it should be marked as executable. + pub(crate) fn new_checked( + bank: &Bank, + program_address: &Pubkey, + ) -> Result { + // The program account should exist. + let program_account = bank + .get_account_with_fixed_root(program_address) + .ok_or(CoreBpfMigrationError::AccountNotFound(*program_address))?; + + // The program account should be owned by the loader v2. + if program_account.owner() != &bpf_loader::id() { + return Err(CoreBpfMigrationError::IncorrectOwner(*program_address)); + } + + // The program account should be executable. + if !program_account.executable() { + return Err(CoreBpfMigrationError::ProgramAccountNotExecutable( + *program_address, + )); + } + + let program_data_address = get_program_data_address(program_address); + + // The program data account should not exist. + if bank + .get_account_with_fixed_root(&program_data_address) + .is_some() + { + return Err(CoreBpfMigrationError::ProgramHasDataAccount( + *program_address, + )); + } + + Ok(Self { + program_address: *program_address, + program_account, + program_data_address, + }) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, crate::bank::tests::create_simple_test_bank, assert_matches::assert_matches, + solana_account::WritableAccount, solana_sdk_ids::bpf_loader, + }; + + fn store_account(bank: &Bank, address: &Pubkey, data: &[u8], owner: &Pubkey, executable: bool) { + let space = data.len(); + let lamports = bank.get_minimum_balance_for_rent_exemption(space); + let mut account = AccountSharedData::new(lamports, space, owner); + account.set_executable(executable); + account.data_as_mut_slice().copy_from_slice(data); + bank.store_account_and_update_capitalization(address, &account); + } + + #[test] + fn test_target_bpf_v2() { + let bank = create_simple_test_bank(0); + + let program_address = Pubkey::new_unique(); + let elf = vec![4u8; 200]; + + // Fail if the program account does not exist. + assert_matches!( + TargetBpfV2::new_checked(&bank, &program_address).unwrap_err(), + CoreBpfMigrationError::AccountNotFound(..) + ); + + // Fail if the program account is not owned by the loader v2. + store_account( + &bank, + &program_address, + &elf, + &Pubkey::new_unique(), // Not the loader v2 + true, + ); + assert_matches!( + TargetBpfV2::new_checked(&bank, &program_address).unwrap_err(), + CoreBpfMigrationError::IncorrectOwner(..) + ); + + // Fail if the program account is not executable. + store_account( + &bank, + &program_address, + &elf, + &bpf_loader::id(), + false, // Not executable + ); + assert_matches!( + TargetBpfV2::new_checked(&bank, &program_address).unwrap_err(), + CoreBpfMigrationError::ProgramAccountNotExecutable(..) + ); + + // Success + store_account(&bank, &program_address, &elf, &bpf_loader::id(), true); + + let target_bpf_v2 = TargetBpfV2::new_checked(&bank, &program_address).unwrap(); + + assert_eq!(target_bpf_v2.program_address, program_address); + assert_eq!(target_bpf_v2.program_account.data(), elf.as_slice()); + } +}