From 67783ce9a07b2f417a916ecf3cf13ea5e31beed9 Mon Sep 17 00:00:00 2001 From: febo Date: Wed, 18 Jun 2025 00:38:00 +0100 Subject: [PATCH 1/8] Add p-token feature --- feature-set/src/lib.rs | 15 +++++++++++++++ svm-feature-set/src/lib.rs | 2 ++ 2 files changed, 17 insertions(+) diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index e59b9d3fe60..e85f5032f5c 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -156,6 +156,8 @@ impl FeatureSet { reenable_zk_elgamal_proof_program: self .is_active(&reenable_zk_elgamal_proof_program::id()), raise_cpi_nesting_limit_to_8: self.is_active(&raise_cpi_nesting_limit_to_8::id()), + migrate_ptoken_to_spl_token_program: self + .is_active(&migrate_ptoken_to_spl_token_program::id()), } } } @@ -1126,6 +1128,18 @@ pub mod enforce_fixed_fec_set { solana_pubkey::declare_id!("fixfecLZYMfkGzwq6NJA11Yw6KYztzXiK9QcL3K78in"); } +pub mod migrate_ptoken_to_spl_token_program { + use solana_pubkey::Pubkey; + + solana_pubkey::declare_id!("ptokSWRqZz5u2xdqMdstkMKpFurauUpVen7TZXgDpkQ"); + + pub const SPL_TOLKEN_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 +1382,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"), + (migrate_ptoken_to_spl_token_program::id(), "SIMD-0266: Efficient Token program"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/svm-feature-set/src/lib.rs b/svm-feature-set/src/lib.rs index dd8e6863419..fd4b8557429 100644 --- a/svm-feature-set/src/lib.rs +++ b/svm-feature-set/src/lib.rs @@ -36,6 +36,7 @@ pub struct SVMFeatureSet { pub disable_zk_elgamal_proof_program: bool, pub reenable_zk_elgamal_proof_program: bool, pub raise_cpi_nesting_limit_to_8: bool, + pub migrate_ptoken_to_spl_token_program: bool, } impl SVMFeatureSet { @@ -77,6 +78,7 @@ impl SVMFeatureSet { disable_zk_elgamal_proof_program: true, reenable_zk_elgamal_proof_program: true, raise_cpi_nesting_limit_to_8: true, + migrate_ptoken_to_spl_token_program: true, } } } From 569a143b7b4d73f4f966f263b0637cdd1a6c5170 Mon Sep 17 00:00:00 2001 From: febo Date: Wed, 18 Jun 2025 00:40:52 +0100 Subject: [PATCH 2/8] Add feature activation --- feature-set/src/lib.rs | 7 +- runtime/src/bank.rs | 16 ++ .../bank/builtins/core_bpf_migration/mod.rs | 184 +++++++++++++++++- .../core_bpf_migration/target_bpf_v2.rs | 134 +++++++++++++ .../core_bpf_migration/target_builtin.rs | 8 +- .../core_bpf_migration/target_core_bpf.rs | 8 +- svm-feature-set/src/lib.rs | 4 +- 7 files changed, 349 insertions(+), 12 deletions(-) create mode 100644 runtime/src/bank/builtins/core_bpf_migration/target_bpf_v2.rs diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index e85f5032f5c..5b8bf6a1106 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -156,8 +156,7 @@ impl FeatureSet { reenable_zk_elgamal_proof_program: self .is_active(&reenable_zk_elgamal_proof_program::id()), raise_cpi_nesting_limit_to_8: self.is_active(&raise_cpi_nesting_limit_to_8::id()), - migrate_ptoken_to_spl_token_program: self - .is_active(&migrate_ptoken_to_spl_token_program::id()), + replace_spl_token_with_p_token: self.is_active(&replace_spl_token_with_p_token::id()), } } } @@ -1128,12 +1127,12 @@ pub mod enforce_fixed_fec_set { solana_pubkey::declare_id!("fixfecLZYMfkGzwq6NJA11Yw6KYztzXiK9QcL3K78in"); } -pub mod migrate_ptoken_to_spl_token_program { +pub mod replace_spl_token_with_p_token { use solana_pubkey::Pubkey; solana_pubkey::declare_id!("ptokSWRqZz5u2xdqMdstkMKpFurauUpVen7TZXgDpkQ"); - pub const SPL_TOLKEN_PROGRAM_ID: Pubkey = + pub const SPL_TOKEN_PROGRAM_ID: Pubkey = Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); pub const PTOKEN_PROGRAM_BUFFER: Pubkey = diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index ee8ab5c5527..941eac1a40d 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -5324,6 +5324,22 @@ 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(&agave_feature_set::replace_spl_token_with_p_token::id()) + { + if let Err(e) = self.upgrade_core_bpf_program( + &agave_feature_set::replace_spl_token_with_p_token::SPL_TOKEN_PROGRAM_ID, + &agave_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 '{}': {}", + agave_feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER, + e + ); + } + } } 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..7feb429a244 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}, @@ -19,7 +20,7 @@ use { 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, @@ -38,16 +39,22 @@ fn checked_sub(a: T, b: T) -> Result { .ok_or(CoreBpfMigrationError::ArithmeticOverflow) } +/// A trait for the target program account being migrated. +pub(crate) trait Target { + /// The target program data address. + fn program_data_address(&self) -> &Pubkey; +} + impl Bank { /// Create an `AccountSharedData` with data initialized to /// `UpgradeableLoaderState::Program` populated with the target's new data /// account address. fn new_target_program_account( &self, - target: &TargetBuiltin, + target: &dyn Target, ) -> Result { let state = UpgradeableLoaderState::Program { - programdata_address: target.program_data_address, + programdata_address: *target.program_data_address(), }; let lamports = self.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()); @@ -218,6 +225,90 @@ impl Bank { Ok(()) } + #[allow(dead_code)] + fn directly_invoke_loader_v2_deploy( + &self, + program_id: &Pubkey, + program_data: &[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.program_cache.read().unwrap(), + ); + + // Configure a dummy `InvokeContext` from the runtime's current + // environment, as well as the two `ProgramCacheForTxBatch` + // instances configured above, then invoke the loader. + { + let compute_budget = self.compute_budget().unwrap_or_default(); + 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 mut 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.saturating_add( + solana_program_runtime::loaded_programs::DELAY_VISIBILITY_SLOT_OFFSET, + )) + .map_err(|_err| { + // This will never fail since the epoch schedule is already configured. + InstructionError::ProgramEnvironmentSetupFailure + })?; + + let load_program_metrics = solana_bpf_loader_program::deploy_program( + dummy_invoke_context.get_log_collector(), + dummy_invoke_context.program_cache_for_tx_batch, + environments.program_runtime_v1.clone(), + program_id, + &bpf_loader::id(), + program_data.len(), + program_data, + self.slot, + )?; + load_program_metrics.submit_datapoint(&mut dummy_invoke_context.timings); + } + + // Update the program cache by merging with `programs_modified`, which + // should have been updated by the deploy function. + self.transaction_processor + .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 +464,91 @@ impl Bank { Ok(()) } + /// Migrate a Loader v2 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.migrate_bpf_loader_v2_to_v3( + /// &bpf_loader_v2_program_address, + /// &source_buffer_address, + /// "test_upgrade_loader_v2_bpf_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 migrate_bpf_loader_v2_to_v3( + &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_target_program_account(&target)?; + // Loader v2 programs do not have an upgrade authority, so pass `None` when + // creating the new program data account. + let new_target_program_data_account = + self.new_target_program_data_account(&source, None)?; + + // 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(), + )?; + let new_data_size = checked_add( + new_target_program_account.data().len(), + new_target_program_data_account.data().len(), + )?; + + // Deploy the new Loader v3 program. + // This step will validate the program ELF against the current runtime + // environment, as well as update the program cache. + self.directly_invoke_loader_v3_deploy( + &target.program_address, + new_target_program_data_account.data(), + )?; + + // Calculate the lamports to burn. + // The target program account will be replaced, so burn its lamports. + // The source buffer account will be cleared, so burn its lamports. + // The two new program accounts will need to be funded. + let lamports_to_burn = checked_add( + target.program_account.lamports(), + source.buffer_account.lamports(), + )?; + let lamports_to_fund = checked_add( + new_target_program_account.lamports(), + new_target_program_data_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( + &target.program_data_address, + &new_target_program_data_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, 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..870e5ae0296 --- /dev/null +++ b/runtime/src/bank/builtins/core_bpf_migration/target_bpf_v2.rs @@ -0,0 +1,134 @@ +#![allow(dead_code)] +use { + super::error::CoreBpfMigrationError, + crate::bank::{builtins::core_bpf_migration::Target, 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, + }) + } +} + +impl Target for TargetBpfV2 { + fn program_data_address(&self) -> &Pubkey { + &self.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()); + } +} diff --git a/runtime/src/bank/builtins/core_bpf_migration/target_builtin.rs b/runtime/src/bank/builtins/core_bpf_migration/target_builtin.rs index 5146a5af95f..454063820ad 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/target_builtin.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/target_builtin.rs @@ -1,6 +1,6 @@ use { super::error::CoreBpfMigrationError, - crate::bank::Bank, + crate::bank::{builtins::core_bpf_migration::Target, Bank}, solana_account::{AccountSharedData, ReadableAccount}, solana_builtins::core_bpf_migration::CoreBpfMigrationTargetType, solana_loader_v3_interface::get_program_data_address, @@ -68,6 +68,12 @@ impl TargetBuiltin { } } +impl Target for TargetBuiltin { + fn program_data_address(&self) -> &Pubkey { + &self.program_data_address + } +} + #[cfg(test)] mod tests { use { diff --git a/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs b/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs index df15b48253b..846154b6bf7 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs @@ -1,6 +1,6 @@ use { super::error::CoreBpfMigrationError, - crate::bank::Bank, + crate::bank::{builtins::core_bpf_migration::Target, Bank}, solana_account::{AccountSharedData, ReadableAccount}, solana_loader_v3_interface::{get_program_data_address, state::UpgradeableLoaderState}, solana_pubkey::Pubkey, @@ -92,6 +92,12 @@ impl TargetCoreBpf { } } +impl Target for TargetCoreBpf { + fn program_data_address(&self) -> &Pubkey { + &self.program_data_address + } +} + #[cfg(test)] mod tests { use { diff --git a/svm-feature-set/src/lib.rs b/svm-feature-set/src/lib.rs index fd4b8557429..d35e8684c86 100644 --- a/svm-feature-set/src/lib.rs +++ b/svm-feature-set/src/lib.rs @@ -36,7 +36,7 @@ pub struct SVMFeatureSet { pub disable_zk_elgamal_proof_program: bool, pub reenable_zk_elgamal_proof_program: bool, pub raise_cpi_nesting_limit_to_8: bool, - pub migrate_ptoken_to_spl_token_program: bool, + pub replace_spl_token_with_p_token: bool, } impl SVMFeatureSet { @@ -78,7 +78,7 @@ impl SVMFeatureSet { disable_zk_elgamal_proof_program: true, reenable_zk_elgamal_proof_program: true, raise_cpi_nesting_limit_to_8: true, - migrate_ptoken_to_spl_token_program: true, + replace_spl_token_with_p_token: true, } } } From a785d522c29fd3305933b59c49c39018984b9f45 Mon Sep 17 00:00:00 2001 From: febo Date: Sun, 20 Jul 2025 12:34:03 +0100 Subject: [PATCH 3/8] Add tests --- feature-set/src/lib.rs | 3 +- runtime/src/bank.rs | 15 +-- .../bank/builtins/core_bpf_migration/mod.rs | 110 ++---------------- .../core_bpf_migration/target_bpf_v2.rs | 8 +- .../core_bpf_migration/target_builtin.rs | 8 +- .../core_bpf_migration/target_core_bpf.rs | 8 +- svm-feature-set/src/lib.rs | 2 - 7 files changed, 21 insertions(+), 133 deletions(-) diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 5b8bf6a1106..f6b02b44857 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -156,7 +156,6 @@ impl FeatureSet { reenable_zk_elgamal_proof_program: self .is_active(&reenable_zk_elgamal_proof_program::id()), raise_cpi_nesting_limit_to_8: self.is_active(&raise_cpi_nesting_limit_to_8::id()), - replace_spl_token_with_p_token: self.is_active(&replace_spl_token_with_p_token::id()), } } } @@ -1128,7 +1127,7 @@ pub mod enforce_fixed_fec_set { } pub mod replace_spl_token_with_p_token { - use solana_pubkey::Pubkey; + use super::Pubkey; solana_pubkey::declare_id!("ptokSWRqZz5u2xdqMdstkMKpFurauUpVen7TZXgDpkQ"); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 941eac1a40d..f7a47446160 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -5325,18 +5325,15 @@ impl Bank { self.apply_simd_0306_cost_tracker_changes(); } - if new_feature_activations - .contains(&agave_feature_set::replace_spl_token_with_p_token::id()) - { - if let Err(e) = self.upgrade_core_bpf_program( - &agave_feature_set::replace_spl_token_with_p_token::SPL_TOKEN_PROGRAM_ID, - &agave_feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER, + if new_feature_activations.contains(&feature_set::replace_spl_token_with_p_token::id()) { + if let Err(e) = self.upgrade_loader_v2_program_with_loader_v3_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 '{}': {}", - agave_feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER, - e + "Failed to replace SPL Token with p-token buffer '{}': {e}", + feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER, ); } } diff --git a/runtime/src/bank/builtins/core_bpf_migration/mod.rs b/runtime/src/bank/builtins/core_bpf_migration/mod.rs index 7feb429a244..a3e758ef49e 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/mod.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/mod.rs @@ -20,7 +20,7 @@ use { sysvar_cache::SysvarCache, }, solana_pubkey::Pubkey, - solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable}, + solana_sdk_ids::bpf_loader_upgradeable, solana_svm_callback::InvokeContextCallback, solana_transaction_context::TransactionContext, source_buffer::SourceBuffer, @@ -39,22 +39,16 @@ fn checked_sub(a: T, b: T) -> Result { .ok_or(CoreBpfMigrationError::ArithmeticOverflow) } -/// A trait for the target program account being migrated. -pub(crate) trait Target { - /// The target program data address. - fn program_data_address(&self) -> &Pubkey; -} - impl Bank { /// Create an `AccountSharedData` with data initialized to /// `UpgradeableLoaderState::Program` populated with the target's new data /// account address. fn new_target_program_account( &self, - target: &dyn Target, + program_data_address: &Pubkey, ) -> Result { let state = UpgradeableLoaderState::Program { - programdata_address: *target.program_data_address(), + programdata_address: *program_data_address, }; let lamports = self.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()); @@ -225,90 +219,6 @@ impl Bank { Ok(()) } - #[allow(dead_code)] - fn directly_invoke_loader_v2_deploy( - &self, - program_id: &Pubkey, - program_data: &[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.program_cache.read().unwrap(), - ); - - // Configure a dummy `InvokeContext` from the runtime's current - // environment, as well as the two `ProgramCacheForTxBatch` - // instances configured above, then invoke the loader. - { - let compute_budget = self.compute_budget().unwrap_or_default(); - 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 mut 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.saturating_add( - solana_program_runtime::loaded_programs::DELAY_VISIBILITY_SLOT_OFFSET, - )) - .map_err(|_err| { - // This will never fail since the epoch schedule is already configured. - InstructionError::ProgramEnvironmentSetupFailure - })?; - - let load_program_metrics = solana_bpf_loader_program::deploy_program( - dummy_invoke_context.get_log_collector(), - dummy_invoke_context.program_cache_for_tx_batch, - environments.program_runtime_v1.clone(), - program_id, - &bpf_loader::id(), - program_data.len(), - program_data, - self.slot, - )?; - load_program_metrics.submit_datapoint(&mut dummy_invoke_context.timings); - } - - // Update the program cache by merging with `programs_modified`, which - // should have been updated by the deploy function. - self.transaction_processor - .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, @@ -329,7 +239,8 @@ impl Bank { }; // Attempt serialization first before modifying the bank. - let new_target_program_account = self.new_target_program_account(&target)?; + let new_target_program_account = + self.new_target_program_account(&target.program_data_address)?; let new_target_program_data_account = self.new_target_program_data_account(&source, config.upgrade_authority_address)?; @@ -464,24 +375,24 @@ impl Bank { Ok(()) } - /// Migrate a Loader v2 BPF program. + /// Upgrade a Loader v2 BPF program to a Loader v3 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.migrate_bpf_loader_v2_to_v3( + /// self.upgrade_loader_v2_program_with_loader_v3_program( /// &bpf_loader_v2_program_address, /// &source_buffer_address, - /// "test_upgrade_loader_v2_bpf_program", + /// "test_upgrade_loader_v2_program_with_loader_v3_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 migrate_bpf_loader_v2_to_v3( + pub(crate) fn upgrade_loader_v2_program_with_loader_v3_program( &mut self, loader_v2_bpf_program_address: &Pubkey, source_buffer_address: &Pubkey, @@ -493,7 +404,8 @@ impl Bank { let source = SourceBuffer::new_checked(self, source_buffer_address)?; // Attempt serialization first before modifying the bank. - let new_target_program_account = self.new_target_program_account(&target)?; + let new_target_program_account = + self.new_target_program_account(&target.program_data_address)?; // Loader v2 programs do not have an upgrade authority, so pass `None` when // creating the new program data account. let new_target_program_data_account = 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 index 870e5ae0296..56ec637e6eb 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/target_bpf_v2.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/target_bpf_v2.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use { super::error::CoreBpfMigrationError, - crate::bank::{builtins::core_bpf_migration::Target, Bank}, + crate::bank::Bank, solana_account::{AccountSharedData, ReadableAccount}, solana_loader_v3_interface::get_program_data_address, solana_pubkey::Pubkey, @@ -62,12 +62,6 @@ impl TargetBpfV2 { } } -impl Target for TargetBpfV2 { - fn program_data_address(&self) -> &Pubkey { - &self.program_data_address - } -} - #[cfg(test)] mod tests { use { diff --git a/runtime/src/bank/builtins/core_bpf_migration/target_builtin.rs b/runtime/src/bank/builtins/core_bpf_migration/target_builtin.rs index 454063820ad..5146a5af95f 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/target_builtin.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/target_builtin.rs @@ -1,6 +1,6 @@ use { super::error::CoreBpfMigrationError, - crate::bank::{builtins::core_bpf_migration::Target, Bank}, + crate::bank::Bank, solana_account::{AccountSharedData, ReadableAccount}, solana_builtins::core_bpf_migration::CoreBpfMigrationTargetType, solana_loader_v3_interface::get_program_data_address, @@ -68,12 +68,6 @@ impl TargetBuiltin { } } -impl Target for TargetBuiltin { - fn program_data_address(&self) -> &Pubkey { - &self.program_data_address - } -} - #[cfg(test)] mod tests { use { diff --git a/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs b/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs index 846154b6bf7..df15b48253b 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs @@ -1,6 +1,6 @@ use { super::error::CoreBpfMigrationError, - crate::bank::{builtins::core_bpf_migration::Target, Bank}, + crate::bank::Bank, solana_account::{AccountSharedData, ReadableAccount}, solana_loader_v3_interface::{get_program_data_address, state::UpgradeableLoaderState}, solana_pubkey::Pubkey, @@ -92,12 +92,6 @@ impl TargetCoreBpf { } } -impl Target for TargetCoreBpf { - fn program_data_address(&self) -> &Pubkey { - &self.program_data_address - } -} - #[cfg(test)] mod tests { use { diff --git a/svm-feature-set/src/lib.rs b/svm-feature-set/src/lib.rs index d35e8684c86..dd8e6863419 100644 --- a/svm-feature-set/src/lib.rs +++ b/svm-feature-set/src/lib.rs @@ -36,7 +36,6 @@ pub struct SVMFeatureSet { pub disable_zk_elgamal_proof_program: bool, pub reenable_zk_elgamal_proof_program: bool, pub raise_cpi_nesting_limit_to_8: bool, - pub replace_spl_token_with_p_token: bool, } impl SVMFeatureSet { @@ -78,7 +77,6 @@ impl SVMFeatureSet { disable_zk_elgamal_proof_program: true, reenable_zk_elgamal_proof_program: true, raise_cpi_nesting_limit_to_8: true, - replace_spl_token_with_p_token: true, } } } From da466b1de688f970fe13c5bfd555215c8c2d3183 Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 22 Aug 2025 01:48:56 +0100 Subject: [PATCH 4/8] Replace program account data --- runtime/src/bank.rs | 2 +- .../bank/builtins/core_bpf_migration/mod.rs | 320 ++++++++++++++---- 2 files changed, 249 insertions(+), 73 deletions(-) diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index f7a47446160..03706dcd5a1 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -5326,7 +5326,7 @@ impl Bank { } if new_feature_activations.contains(&feature_set::replace_spl_token_with_p_token::id()) { - if let Err(e) = self.upgrade_loader_v2_program_with_loader_v3_program( + 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", diff --git a/runtime/src/bank/builtins/core_bpf_migration/mod.rs b/runtime/src/bank/builtins/core_bpf_migration/mod.rs index a3e758ef49e..36bad0267ad 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/mod.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/mod.rs @@ -16,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, }; @@ -116,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 @@ -219,6 +247,118 @@ 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 + })?; + + /* + let current = dummy_invoke_context + .program_cache_for_tx_batch + .find(program_id) + .ok_or(InstructionError::IncorrectProgramId)?; + + // Sanity check: make sure we got the correct account owner + let program_owner = if current.account_owner == ProgramCacheEntryOwner::LoaderV2 { + current.account_owner() + } else { + return Err(InstructionError::InvalidAccountOwner); + }; + */ + + // 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::AccountAlreadyInitialized)?; + + 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, @@ -375,24 +515,24 @@ impl Bank { Ok(()) } - /// Upgrade a Loader v2 BPF program to a Loader v3 BPF program. + /// 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_program_with_loader_v3_program( + /// self.upgrade_loader_v2_owned_program( /// &bpf_loader_v2_program_address, /// &source_buffer_address, - /// "test_upgrade_loader_v2_program_with_loader_v3_program", + /// "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_program_with_loader_v3_program( + pub(crate) fn upgrade_loader_v2_owned_program( &mut self, loader_v2_bpf_program_address: &Pubkey, source_buffer_address: &Pubkey, @@ -404,12 +544,7 @@ impl Bank { let source = SourceBuffer::new_checked(self, source_buffer_address)?; // Attempt serialization first before modifying the bank. - let new_target_program_account = - self.new_target_program_account(&target.program_data_address)?; - // Loader v2 programs do not have an upgrade authority, so pass `None` when - // creating the new program data account. - let new_target_program_data_account = - self.new_target_program_data_account(&source, None)?; + 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. @@ -420,39 +555,28 @@ impl Bank { target.program_account.data().len(), source.buffer_account.data().len(), )?; - let new_data_size = checked_add( - new_target_program_account.data().len(), - new_target_program_data_account.data().len(), - )?; + // Size of the buffer is always greater than the metadata size. + let new_data_size = new_target_program_account.data().len(); - // Deploy the new Loader v3 program. - // This step will validate the program ELF against the current runtime - // environment, as well as update the program cache. - self.directly_invoke_loader_v3_deploy( + // Replaces the program account data. + self.replace_loader_v2_owned_program_data( &target.program_address, - new_target_program_data_account.data(), + new_target_program_account.data(), )?; // Calculate the lamports to burn. - // The target program account will be replaced, so burn its lamports. + // The target program account will change size, so burn its lamports. // The source buffer account will be cleared, so burn its lamports. - // The two new program accounts will need to be funded. + // 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 = checked_add( - new_target_program_account.lamports(), - new_target_program_data_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( - &target.program_data_address, - &new_target_program_data_account, - ); self.store_account(&source.buffer_address, &AccountSharedData::default()); // Update the account data size delta. @@ -613,6 +737,33 @@ pub(crate) mod tests { ) } + // Given a bank, calculate the expected capitalization and accounts data + // size delta off-chain after a migration, using the values stored in + // the test context. + pub(crate) fn calculate_post_migration_capitalization_and_accounts_data_size_delta_off_chain_for_loader_v2( + &self, + bank: &Bank, + ) -> (u64, i64) { + let builtin_account = bank + .get_account(&self.target_program_address) + .unwrap_or_default(); + let source_buffer_account = bank.get_account(&self.source_buffer_address).unwrap(); + let resulting_program_data_len = self.elf.len(); + let expected_post_migration_capitalization = bank.capitalization() + - builtin_account.lamports() + - source_buffer_account.lamports() + + bank.get_minimum_balance_for_rent_exemption(resulting_program_data_len); + let expected_post_migration_accounts_data_size_delta_off_chain = + bank.accounts_data_size_delta_off_chain.load(Relaxed) + + resulting_program_data_len as i64 + - builtin_account.data().len() as i64 + - source_buffer_account.data().len() as i64; + ( + expected_post_migration_capitalization, + expected_post_migration_accounts_data_size_delta_off_chain, + ) + } + // Given a bank, calculate the expected capitalization and accounts data // size delta off-chain after an upgrade, using the values stored in // the test context. @@ -648,7 +799,12 @@ pub(crate) mod tests { // * The source buffer account is cleared. // * The bank's builtin IDs do not contain the target program address. // * The cache contains the target program, and the entry is updated. - pub(crate) fn run_program_checks(&self, bank: &Bank, migration_or_upgrade_slot: Slot) { + pub(crate) fn run_program_checks( + &self, + bank: &Bank, + migration_or_upgrade_slot: Slot, + program_owner: &Pubkey, + ) { // Verify the source buffer account has been cleared. assert!(bank.get_account(&self.source_buffer_address).is_none()); @@ -656,45 +812,60 @@ pub(crate) mod tests { let program_data_address = get_program_data_address(&self.target_program_address); // Program account is owned by the upgradeable loader. - assert_eq!(program_account.owner(), &bpf_loader_upgradeable::id()); + assert_eq!(program_account.owner(), program_owner); // Program account is executable. assert!(program_account.executable()); - // Program account has the correct state, with a pointer to its program - // data address. - let program_account_state: UpgradeableLoaderState = program_account.state().unwrap(); - assert_eq!( - program_account_state, - UpgradeableLoaderState::Program { - programdata_address: program_data_address + let (program_data_account, slot) = match *program_owner { + bpf_loader::ID => { + assert_eq!(&program_account.data(), &self.elf,); + (None, migration_or_upgrade_slot) } - ); - - let program_data_account = bank.get_account(&program_data_address).unwrap(); - - // Program data account is owned by the upgradeable loader. - assert_eq!(program_data_account.owner(), &bpf_loader_upgradeable::id()); - - // Program data account has the correct state. - // It should have the same update authority and ELF as the source - // buffer account. - // The slot should be the slot it was migrated at. - let programdata_metadata_size = UpgradeableLoaderState::size_of_programdata_metadata(); - let program_data_account_state_metadata: UpgradeableLoaderState = - bincode::deserialize(&program_data_account.data()[..programdata_metadata_size]) - .unwrap(); - assert_eq!( - program_data_account_state_metadata, - UpgradeableLoaderState::ProgramData { - slot: migration_or_upgrade_slot, - upgrade_authority_address: self.upgrade_authority_address // Preserved - }, - ); - assert_eq!( - &program_data_account.data()[programdata_metadata_size..], - &self.elf, - ); + bpf_loader_upgradeable::ID => { + // Program account has the correct state, with a pointer to its program + // data address. + let program_account_state: UpgradeableLoaderState = + program_account.state().unwrap(); + assert_eq!( + program_account_state, + UpgradeableLoaderState::Program { + programdata_address: program_data_address + } + ); + + let program_data_account = bank.get_account(&program_data_address).unwrap(); + + // Program data account is owned by the upgradeable loader. + assert_eq!(program_data_account.owner(), &bpf_loader_upgradeable::id()); + + // Program data account has the correct state. + // It should have the same update authority and ELF as the source + // buffer account. + // The slot should be the slot it was migrated at. + let programdata_metadata_size = + UpgradeableLoaderState::size_of_programdata_metadata(); + let program_data_account_state_metadata: UpgradeableLoaderState = + bincode::deserialize( + &program_data_account.data()[..programdata_metadata_size], + ) + .unwrap(); + assert_eq!( + program_data_account_state_metadata, + UpgradeableLoaderState::ProgramData { + slot: migration_or_upgrade_slot, + upgrade_authority_address: self.upgrade_authority_address // Preserved + }, + ); + assert_eq!( + &program_data_account.data()[programdata_metadata_size..], + &self.elf, + ); + + (Some(program_data_account), migration_or_upgrade_slot + 1) + } + _ => panic!("Unexpected program owner: {}", program_owner), + }; // The bank's builtins should not contain the target program // address. @@ -719,9 +890,14 @@ pub(crate) mod tests { .unwrap(); // The target program entry should be updated. - assert_eq!(target_entry.account_size, program_data_account.data().len()); + if program_data_account.is_some() { + assert_eq!( + target_entry.account_size, + program_data_account.unwrap().data().len() + ); + } assert_eq!(target_entry.deployment_slot, migration_or_upgrade_slot); - assert_eq!(target_entry.effective_slot, migration_or_upgrade_slot + 1); + assert_eq!(target_entry.effective_slot, slot); // The target program entry should be a BPF program. assert_matches!(target_entry.program, ProgramCacheEntryType::Loaded(..)); @@ -786,7 +962,7 @@ pub(crate) mod tests { .unwrap(); // Run the post-migration program checks. - test_context.run_program_checks(&bank, migration_slot); + test_context.run_program_checks(&bank, migration_slot, &bpf_loader_upgradeable::id()); // Check the bank's capitalization. assert_eq!( @@ -851,7 +1027,7 @@ pub(crate) mod tests { .unwrap(); // Run the post-migration program checks. - test_context.run_program_checks(&bank, migration_slot); + test_context.run_program_checks(&bank, migration_slot, &bpf_loader_upgradeable::id()); // Check the bank's capitalization. assert_eq!( @@ -1134,7 +1310,7 @@ pub(crate) mod tests { .unwrap(); // Run the post-upgrade program checks. - test_context.run_program_checks(&bank, upgrade_slot); + test_context.run_program_checks(&bank, upgrade_slot, &bpf_loader_upgradeable::id()); // Check the bank's capitalization. assert_eq!(bank.capitalization(), expected_post_upgrade_capitalization); From d28b4aed4a806411200bec8259eca6118cdb8112 Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 22 Aug 2025 11:23:47 +0100 Subject: [PATCH 5/8] Revert changes --- .../bank/builtins/core_bpf_migration/mod.rs | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/runtime/src/bank/builtins/core_bpf_migration/mod.rs b/runtime/src/bank/builtins/core_bpf_migration/mod.rs index 36bad0267ad..a7012101808 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/mod.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/mod.rs @@ -48,10 +48,10 @@ impl Bank { /// account address. fn new_target_program_account( &self, - program_data_address: &Pubkey, + target: &TargetBuiltin, ) -> Result { let state = UpgradeableLoaderState::Program { - programdata_address: *program_data_address, + programdata_address: target.program_data_address, }; let lamports = self.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()); @@ -316,20 +316,6 @@ impl Bank { InstructionError::ProgramEnvironmentSetupFailure })?; - /* - let current = dummy_invoke_context - .program_cache_for_tx_batch - .find(program_id) - .ok_or(InstructionError::IncorrectProgramId)?; - - // Sanity check: make sure we got the correct account owner - let program_owner = if current.account_owner == ProgramCacheEntryOwner::LoaderV2 { - current.account_owner() - } else { - return Err(InstructionError::InvalidAccountOwner); - }; - */ - // We need to pass one instance of `LoadProgramMetrics` to create a new // cache entry. let mut metrics = LoadProgramMetrics::default(); @@ -379,8 +365,7 @@ impl Bank { }; // Attempt serialization first before modifying the bank. - let new_target_program_account = - self.new_target_program_account(&target.program_data_address)?; + let new_target_program_account = self.new_target_program_account(&target)?; let new_target_program_data_account = self.new_target_program_data_account(&source, config.upgrade_authority_address)?; From 4dcfd2d156918541b0b833cec7b76d72a868efac Mon Sep 17 00:00:00 2001 From: febo Date: Fri, 22 Aug 2025 11:28:24 +0100 Subject: [PATCH 6/8] Replace error type --- runtime/src/bank/builtins/core_bpf_migration/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/bank/builtins/core_bpf_migration/mod.rs b/runtime/src/bank/builtins/core_bpf_migration/mod.rs index a7012101808..dfea1aba55a 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/mod.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/mod.rs @@ -329,7 +329,7 @@ impl Bank { programdata.len(), &mut metrics, ) - .map_err(|_err| InstructionError::AccountAlreadyInitialized)?; + .map_err(|_err| InstructionError::ProgramEnvironmentSetupFailure)?; program_cache_for_tx_batch.store_modified_entry(*program_id, Arc::new(updated)); } From dd91b150e5f82d2386eee0a32faf77419436195a Mon Sep 17 00:00:00 2001 From: febo Date: Sun, 31 Aug 2025 01:33:09 +0100 Subject: [PATCH 7/8] Update tests --- .../bank/builtins/core_bpf_migration/mod.rs | 558 +++++++++++++----- 1 file changed, 406 insertions(+), 152 deletions(-) diff --git a/runtime/src/bank/builtins/core_bpf_migration/mod.rs b/runtime/src/bank/builtins/core_bpf_migration/mod.rs index dfea1aba55a..fee41441f16 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/mod.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/mod.rs @@ -617,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}, @@ -722,33 +723,6 @@ pub(crate) mod tests { ) } - // Given a bank, calculate the expected capitalization and accounts data - // size delta off-chain after a migration, using the values stored in - // the test context. - pub(crate) fn calculate_post_migration_capitalization_and_accounts_data_size_delta_off_chain_for_loader_v2( - &self, - bank: &Bank, - ) -> (u64, i64) { - let builtin_account = bank - .get_account(&self.target_program_address) - .unwrap_or_default(); - let source_buffer_account = bank.get_account(&self.source_buffer_address).unwrap(); - let resulting_program_data_len = self.elf.len(); - let expected_post_migration_capitalization = bank.capitalization() - - builtin_account.lamports() - - source_buffer_account.lamports() - + bank.get_minimum_balance_for_rent_exemption(resulting_program_data_len); - let expected_post_migration_accounts_data_size_delta_off_chain = - bank.accounts_data_size_delta_off_chain.load(Relaxed) - + resulting_program_data_len as i64 - - builtin_account.data().len() as i64 - - source_buffer_account.data().len() as i64; - ( - expected_post_migration_capitalization, - expected_post_migration_accounts_data_size_delta_off_chain, - ) - } - // Given a bank, calculate the expected capitalization and accounts data // size delta off-chain after an upgrade, using the values stored in // the test context. @@ -784,12 +758,7 @@ pub(crate) mod tests { // * The source buffer account is cleared. // * The bank's builtin IDs do not contain the target program address. // * The cache contains the target program, and the entry is updated. - pub(crate) fn run_program_checks( - &self, - bank: &Bank, - migration_or_upgrade_slot: Slot, - program_owner: &Pubkey, - ) { + pub(crate) fn run_program_checks(&self, bank: &Bank, migration_or_upgrade_slot: Slot) { // Verify the source buffer account has been cleared. assert!(bank.get_account(&self.source_buffer_address).is_none()); @@ -797,60 +766,45 @@ pub(crate) mod tests { let program_data_address = get_program_data_address(&self.target_program_address); // Program account is owned by the upgradeable loader. - assert_eq!(program_account.owner(), program_owner); + assert_eq!(program_account.owner(), &bpf_loader_upgradeable::id()); // Program account is executable. assert!(program_account.executable()); - let (program_data_account, slot) = match *program_owner { - bpf_loader::ID => { - assert_eq!(&program_account.data(), &self.elf,); - (None, migration_or_upgrade_slot) - } - bpf_loader_upgradeable::ID => { - // Program account has the correct state, with a pointer to its program - // data address. - let program_account_state: UpgradeableLoaderState = - program_account.state().unwrap(); - assert_eq!( - program_account_state, - UpgradeableLoaderState::Program { - programdata_address: program_data_address - } - ); - - let program_data_account = bank.get_account(&program_data_address).unwrap(); - - // Program data account is owned by the upgradeable loader. - assert_eq!(program_data_account.owner(), &bpf_loader_upgradeable::id()); - - // Program data account has the correct state. - // It should have the same update authority and ELF as the source - // buffer account. - // The slot should be the slot it was migrated at. - let programdata_metadata_size = - UpgradeableLoaderState::size_of_programdata_metadata(); - let program_data_account_state_metadata: UpgradeableLoaderState = - bincode::deserialize( - &program_data_account.data()[..programdata_metadata_size], - ) - .unwrap(); - assert_eq!( - program_data_account_state_metadata, - UpgradeableLoaderState::ProgramData { - slot: migration_or_upgrade_slot, - upgrade_authority_address: self.upgrade_authority_address // Preserved - }, - ); - assert_eq!( - &program_data_account.data()[programdata_metadata_size..], - &self.elf, - ); - - (Some(program_data_account), migration_or_upgrade_slot + 1) + // Program account has the correct state, with a pointer to its program + // data address. + let program_account_state: UpgradeableLoaderState = program_account.state().unwrap(); + assert_eq!( + program_account_state, + UpgradeableLoaderState::Program { + programdata_address: program_data_address } - _ => panic!("Unexpected program owner: {}", program_owner), - }; + ); + + let program_data_account = bank.get_account(&program_data_address).unwrap(); + + // Program data account is owned by the upgradeable loader. + assert_eq!(program_data_account.owner(), &bpf_loader_upgradeable::id()); + + // Program data account has the correct state. + // It should have the same update authority and ELF as the source + // buffer account. + // The slot should be the slot it was migrated at. + let programdata_metadata_size = UpgradeableLoaderState::size_of_programdata_metadata(); + let program_data_account_state_metadata: UpgradeableLoaderState = + bincode::deserialize(&program_data_account.data()[..programdata_metadata_size]) + .unwrap(); + assert_eq!( + program_data_account_state_metadata, + UpgradeableLoaderState::ProgramData { + slot: migration_or_upgrade_slot, + upgrade_authority_address: self.upgrade_authority_address // Preserved + }, + ); + assert_eq!( + &program_data_account.data()[programdata_metadata_size..], + &self.elf, + ); // The bank's builtins should not contain the target program // address. @@ -875,14 +829,9 @@ pub(crate) mod tests { .unwrap(); // The target program entry should be updated. - if program_data_account.is_some() { - assert_eq!( - target_entry.account_size, - program_data_account.unwrap().data().len() - ); - } + assert_eq!(target_entry.account_size, program_data_account.data().len()); assert_eq!(target_entry.deployment_slot, migration_or_upgrade_slot); - assert_eq!(target_entry.effective_slot, slot); + assert_eq!(target_entry.effective_slot, migration_or_upgrade_slot + 1); // The target program entry should be a BPF program. assert_matches!(target_entry.program, ProgramCacheEntryType::Loaded(..)); @@ -947,7 +896,7 @@ pub(crate) mod tests { .unwrap(); // Run the post-migration program checks. - test_context.run_program_checks(&bank, migration_slot, &bpf_loader_upgradeable::id()); + test_context.run_program_checks(&bank, migration_slot); // Check the bank's capitalization. assert_eq!( @@ -1012,7 +961,7 @@ pub(crate) mod tests { .unwrap(); // Run the post-migration program checks. - test_context.run_program_checks(&bank, migration_slot, &bpf_loader_upgradeable::id()); + test_context.run_program_checks(&bank, migration_slot); // Check the bank's capitalization. assert_eq!( @@ -1295,7 +1244,7 @@ pub(crate) mod tests { .unwrap(); // Run the post-upgrade program checks. - test_context.run_program_checks(&bank, upgrade_slot, &bpf_loader_upgradeable::id()); + test_context.run_program_checks(&bank, upgrade_slot); // Check the bank's capitalization. assert_eq!(bank.capitalization(), expected_post_upgrade_capitalization); @@ -1411,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. @@ -1497,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()), ), @@ -1544,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()), ), @@ -1571,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. @@ -1874,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() + ); + } } From 1450d175efb0034829b57e782cab502b3e68af2c Mon Sep 17 00:00:00 2001 From: febo Date: Sun, 31 Aug 2025 01:43:19 +0100 Subject: [PATCH 8/8] Update feature module --- feature-set/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index f6b02b44857..4b9374db8b2 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -1380,7 +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"), - (migrate_ptoken_to_spl_token_program::id(), "SIMD-0266: Efficient Token program"), + (replace_spl_token_with_p_token::id(), "SIMD-0266: Efficient Token program"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter()