diff --git a/runtime/src/bank/migrate_native_program/bpf_upgradeable_program.rs b/runtime/src/bank/migrate_native_program/bpf_upgradeable_program.rs index bacf898a866ffb..5b26178aff0910 100644 --- a/runtime/src/bank/migrate_native_program/bpf_upgradeable_program.rs +++ b/runtime/src/bank/migrate_native_program/bpf_upgradeable_program.rs @@ -39,20 +39,18 @@ fn check_upgradeable_loader_state( /// /// This struct is used to validate the BPF upgradeable program's account and /// data account before the migration is performed. -#[allow(dead_code)] // TODO: Removed in future commit #[derive(Debug)] -struct BpfUpgradeableProgramConfig { - program_address: Pubkey, - program_account: Account, - program_data_address: Pubkey, - program_data_account: Account, - total_data_size: usize, +pub struct BpfUpgradeableProgramConfig { + pub program_address: Pubkey, + pub program_account: Account, + pub program_data_address: Pubkey, + pub program_data_account: Account, + pub total_data_size: usize, } -#[allow(dead_code)] // TODO: Removed in future commit impl BpfUpgradeableProgramConfig { /// Creates a new migration config for the given BPF upgradeable program, /// validating the BPF program's account and data account - fn new_checked(bank: &Bank, address: &Pubkey) -> Result { + pub fn new_checked(bank: &Bank, address: &Pubkey) -> Result { let program_address = *address; let program_account: Account = bank .get_account_with_fixed_root(&program_address) diff --git a/runtime/src/bank/migrate_native_program/mod.rs b/runtime/src/bank/migrate_native_program/mod.rs index ee6b919fc8eca3..d41f10e41d7e75 100644 --- a/runtime/src/bank/migrate_native_program/mod.rs +++ b/runtime/src/bank/migrate_native_program/mod.rs @@ -9,7 +9,12 @@ use { native_program::NativeProgramConfig, }, super::Bank, - solana_sdk::{account::AccountSharedData, pubkey::Pubkey}, + crate::bank::migrate_native_program::bpf_upgradeable_program::BpfUpgradeableProgramConfig, + solana_sdk::{ + account::{Account, AccountSharedData}, + bpf_loader_upgradeable::{UpgradeableLoaderState, ID as BPF_LOADER_UPGRADEABLE_ID}, + pubkey::Pubkey, + }, std::sync::atomic::Ordering::Relaxed, }; @@ -106,3 +111,72 @@ pub(crate) fn migrate_native_program_to_bpf_non_upgradeable( Ok(()) } + +/// Create a new `Account` with a pointer to the target's new data account. +/// +/// Note the pointer is created manually, as well as the owner and +/// executable values. The rest is inherited from the source program +/// account, including the lamports. +fn create_new_target_program_account( + target: &NativeProgramConfig, + source: &BpfUpgradeableProgramConfig, +) -> Result { + let state = UpgradeableLoaderState::Program { + programdata_address: target.program_data_address, + }; + let data = bincode::serialize(&state)?; + let account = Account { + data, + owner: BPF_LOADER_UPGRADEABLE_ID, + executable: true, + ..source.program_account + }; + Ok(AccountSharedData::from(account)) +} + +/// Migrate a native program to an upgradeable BPF program using a BPF version +/// of the program deployed at some arbitrary address. +#[allow(dead_code)] // Code is off the hot path until a migration is due +pub(crate) fn migrate_native_program_to_bpf_upgradeable( + bank: &Bank, + target_program: NativeProgram, + source_program_address: &Pubkey, + datapoint_name: &'static str, +) -> Result<(), MigrateNativeProgramError> { + datapoint_info!(datapoint_name, ("slot", bank.slot, i64)); + + let target = NativeProgramConfig::new_checked(bank, target_program)?; + let source = BpfUpgradeableProgramConfig::new_checked(bank, source_program_address)?; + + // Attempt serialization first before touching the bank + let new_target_program_account = create_new_target_program_account(&target, &source)?; + + // Burn lamports from the target program account + bank.capitalization + .fetch_sub(target.program_account.lamports, Relaxed); + + // Replace the native program account with the created to point to the new data + // account and clear the source program account + bank.store_account(&target.program_address, &new_target_program_account); + bank.store_account(&source.program_address, &AccountSharedData::default()); + + // Copy the upgradeable BPF program's data account into the native + // program's data address, which is checked to be empty, then clear the + // upgradeable BPF program's data account. + bank.store_account(&target.program_data_address, &source.program_data_account); + bank.store_account(&source.program_data_address, &AccountSharedData::default()); + + // Update the account data size delta. + bank.calculate_and_update_accounts_data_size_delta_off_chain( + target.total_data_size, + source.total_data_size, + ); + + // Unload the programs from the bank's cache + bank.loaded_programs_cache + .write() + .unwrap() + .remove_programs([source.program_address, target.program_address].into_iter()); + + Ok(()) +} diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index cabbc7d3a1adb5..325665139d5de9 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -9,7 +9,8 @@ use { crate::{ accounts_background_service::{PrunedBanksRequestHandler, SendDroppedBankCallback}, bank::migrate_native_program::{ - migrate_native_program_to_bpf_non_upgradeable, NativeProgram, + migrate_native_program_to_bpf_non_upgradeable, + migrate_native_program_to_bpf_upgradeable, NativeProgram, }, bank_client::BankClient, bank_forks::BankForks, @@ -59,7 +60,7 @@ use { }, account_utils::StateMut, bpf_loader, - bpf_loader_upgradeable::{self, UpgradeableLoaderState}, + bpf_loader_upgradeable::{self, get_program_data_address, UpgradeableLoaderState}, client::SyncClient, clock::{ BankId, Epoch, Slot, UnixTimestamp, DEFAULT_HASHES_PER_TICK, DEFAULT_SLOTS_PER_EPOCH, @@ -8049,6 +8050,92 @@ fn test_migrate_native_program_to_bpf_non_upgradeable(target: NativeProgram) { assert_eq!(bank.capitalization(), expected_capitalization); } +#[test_case(NativeProgram::AddressLookupTable)] +#[test_case(NativeProgram::BpfLoader)] +#[test_case(NativeProgram::BpfLoaderUpgradeable)] +#[test_case(NativeProgram::ComputeBudget)] +#[test_case(NativeProgram::Config)] +#[test_case(NativeProgram::Ed25519)] +#[test_case(NativeProgram::FeatureGate)] +#[test_case(NativeProgram::LoaderV4)] +#[test_case(NativeProgram::NativeLoader)] +#[test_case(NativeProgram::Secp256k1)] +#[test_case(NativeProgram::Stake)] +#[test_case(NativeProgram::System)] +#[test_case(NativeProgram::Vote)] +#[test_case(NativeProgram::ZkTokenProof)] +fn test_migrate_native_program_to_bpf_upgradeable(target: NativeProgram) { + let bank = create_simple_test_bank(0); + + let target_program_address = target.id(); + let (target_program_data_address, _) = get_program_data_address(&target_program_address); + + let source_program_address = Pubkey::new_unique(); + let (source_program_data_address, _) = get_program_data_address(&source_program_address); + + setup_program_account_for_tests( + &bank, + &source_program_address, + &UpgradeableLoaderState::Program { + programdata_address: source_program_data_address, + }, + &bpf_loader_upgradeable::id(), + true, + ); + setup_program_account_for_tests( + &bank, + &source_program_data_address, + &vec![4u8; 300], + &bpf_loader_upgradeable::id(), + false, + ); + + let original_source_program_account = bank.get_account(&source_program_address).unwrap(); + let original_source_program_data_account = + bank.get_account(&source_program_data_address).unwrap(); + let expected_capitalization = bank.capitalization() + - bank + .get_account(&target_program_address) + .map(|account| account.lamports()) + .unwrap_or_default(); + + // Perform the migration + migrate_native_program_to_bpf_upgradeable( + &bank, + target, + &source_program_address, + "migrate_native_program", + ) + .unwrap(); + + // Assert the new target account holds a pointer to its data account + let expected_data = bincode::serialize(&UpgradeableLoaderState::Program { + programdata_address: target_program_data_address, + }) + .unwrap(); + let post_migration_target_account = bank.get_account(&target_program_address).unwrap(); + assert_eq!(post_migration_target_account.data(), &expected_data); + + // Assert the target data account is the same as the original source data account + let post_migration_target_data_account = + bank.get_account(&target_program_data_address).unwrap(); + assert_eq!( + original_source_program_data_account, + post_migration_target_data_account + ); + + // Assert the source account was cleared + let post_migration_source_account = bank.get_account(&source_program_address); + assert!(post_migration_source_account.is_none()); + + // Assert the source data account was cleared + let post_migration_source_data_account = bank.get_account(&source_program_data_address); + assert!(post_migration_source_data_account.is_none()); + + // Assert the lamports of the target account were burnt + assert_eq!(bank.capitalization(), expected_capitalization); +} + #[test] fn test_adjust_sysvar_balance_for_rent() { let bank = create_simple_test_bank(0);