diff --git a/programs/bpf_loader/Cargo.toml b/programs/bpf_loader/Cargo.toml index 0503c51a8f1419..2f35e3c2627ad5 100644 --- a/programs/bpf_loader/Cargo.toml +++ b/programs/bpf_loader/Cargo.toml @@ -29,7 +29,7 @@ solana-hash = { workspace = true } solana-instruction = { workspace = true } solana-keccak-hasher = { workspace = true } solana-loader-v3-interface = { workspace = true, features = ["serde"] } -solana-loader-v4-interface = { workspace = true, optional = true } +solana-loader-v4-interface = { workspace = true, features = ["bincode"] } solana-log-collector = { workspace = true } solana-measure = { workspace = true } solana-packet = { workspace = true } @@ -92,4 +92,4 @@ shuttle-test = [ "solana-program-runtime/shuttle-test", "solana-sbpf/shuttle-test" ] -svm-internal = ["dep:solana-loader-v4-interface"] +svm-internal = [] diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index 8478abcc3ca6ee..f117d6d0df470d 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -13,7 +13,8 @@ use { solana_compute_budget::compute_budget::MAX_INSTRUCTION_STACK_DEPTH, solana_feature_set::{ bpf_account_data_direct_mapping, disable_new_loader_v3_deployments, - enable_bpf_loader_set_authority_checked_ix, remove_accounts_executable_flag_checks, + enable_bpf_loader_set_authority_checked_ix, enable_loader_v4, + remove_accounts_executable_flag_checks, }, solana_instruction::{error::InstructionError, AccountMeta}, solana_loader_v3_interface::{ @@ -43,7 +44,10 @@ use { verifier::RequisiteVerifier, vm::{ContextObject, EbpfVm}, }, - solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, native_loader}, + solana_sdk_ids::{ + bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader, + system_program, + }, solana_system_interface::{instruction as system_instruction, MAX_PERMITTED_DATA_LENGTH}, solana_transaction_context::{IndexOfAccount, InstructionContext, TransactionContext}, solana_type_overrides::sync::{atomic::Ordering, Arc}, @@ -393,6 +397,10 @@ declare_builtin_function!( } ); +mod migration_authority { + solana_pubkey::declare_id!("3Scf35jMNk2xXBD6areNjgMtXgp5ZspDhms8vdcbzC42"); +} + #[cfg_attr(feature = "svm-internal", qualifiers(pub))] pub(crate) fn process_instruction_inner( invoke_context: &mut InvokeContext, @@ -1336,6 +1344,176 @@ fn process_loader_upgradeable_instruction( additional_bytes ); } + UpgradeableLoaderInstruction::Migrate => { + if !invoke_context + .get_feature_set() + .is_active(&enable_loader_v4::id()) + { + return Err(InstructionError::InvalidInstructionData); + } + + instruction_context.check_number_of_instruction_accounts(3)?; + let programdata_address = *transaction_context.get_key_of_account_at_index( + instruction_context.get_index_of_instruction_account_in_transaction(0)?, + )?; + let program_address = *transaction_context.get_key_of_account_at_index( + instruction_context.get_index_of_instruction_account_in_transaction(1)?, + )?; + let provided_authority_address = *transaction_context.get_key_of_account_at_index( + instruction_context.get_index_of_instruction_account_in_transaction(2)?, + )?; + let clock_slot = invoke_context + .get_sysvar_cache() + .get_clock() + .map(|clock| clock.slot)?; + + // Verify ProgramData account + let programdata = + instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + if !programdata.is_writable() { + ic_logger_msg!(log_collector, "ProgramData account not writeable"); + return Err(InstructionError::InvalidArgument); + } + let (program_len, upgrade_authority_address) = + if let Ok(UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address, + }) = programdata.get_state() + { + if clock_slot == slot { + ic_logger_msg!(log_collector, "Program was deployed in this block already"); + return Err(InstructionError::InvalidArgument); + } + ( + programdata + .get_data() + .len() + .saturating_sub(UpgradeableLoaderState::size_of_programdata_metadata()), + upgrade_authority_address, + ) + } else { + (0, None) + }; + let programdata_funds = programdata.get_lamports(); + drop(programdata); + + // Verify authority signature + if !migration_authority::check_id(&provided_authority_address) + && provided_authority_address + != upgrade_authority_address.unwrap_or(program_address) + { + ic_logger_msg!(log_collector, "Incorrect migration authority provided"); + return Err(InstructionError::IncorrectAuthority); + } + if !instruction_context.is_instruction_account_signer(2)? { + ic_logger_msg!(log_collector, "Migration authority did not sign"); + return Err(InstructionError::MissingRequiredSignature); + } + + // Verify Program account + let mut program = + instruction_context.try_borrow_instruction_account(transaction_context, 1)?; + if !program.is_writable() { + ic_logger_msg!(log_collector, "Program account not writeable"); + return Err(InstructionError::InvalidArgument); + } + if program.get_owner() != program_id { + ic_logger_msg!(log_collector, "Program account not owned by loader"); + return Err(InstructionError::IncorrectProgramId); + } + if let UpgradeableLoaderState::Program { + programdata_address: stored_programdata_address, + } = program.get_state()? + { + if programdata_address != stored_programdata_address { + ic_logger_msg!(log_collector, "Program and ProgramData account mismatch"); + return Err(InstructionError::InvalidArgument); + } + } else { + ic_logger_msg!(log_collector, "Invalid Program account"); + return Err(InstructionError::InvalidAccountData); + } + program.set_data_from_slice(&[])?; + program.checked_add_lamports(programdata_funds)?; + if program_len == 0 { + program.set_owner(&system_program::id().to_bytes())?; + } else { + program.set_owner(&loader_v4::id().to_bytes())?; + } + drop(program); + + let mut programdata = + instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + programdata.set_lamports(0)?; + drop(programdata); + + if program_len > 0 { + invoke_context.native_invoke( + solana_loader_v4_interface::instruction::set_program_length( + &program_address, + &provided_authority_address, + program_len as u32, + &program_address, + ) + .into(), + &[], + )?; + + invoke_context.native_invoke( + solana_loader_v4_interface::instruction::copy( + &program_address, + &provided_authority_address, + &programdata_address, + 0, + 0, + program_len as u32, + ) + .into(), + &[], + )?; + + invoke_context.native_invoke( + solana_loader_v4_interface::instruction::deploy( + &program_address, + &provided_authority_address, + ) + .into(), + &[], + )?; + + if upgrade_authority_address.is_none() { + invoke_context.native_invoke( + solana_loader_v4_interface::instruction::finalize( + &program_address, + &provided_authority_address, + &program_address, + ) + .into(), + &[], + )?; + } else if migration_authority::check_id(&provided_authority_address) { + invoke_context.native_invoke( + solana_loader_v4_interface::instruction::transfer_authority( + &program_address, + &provided_authority_address, + &upgrade_authority_address.unwrap(), + ) + .into(), + &[], + )?; + } + } + + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let mut programdata = + instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + programdata.set_data_from_slice(&[])?; + programdata.set_owner(&system_program::id().to_bytes())?; + drop(programdata); + + ic_logger_msg!(log_collector, "Migrated program {:?}", &program_address); + } } Ok(()) @@ -1669,7 +1847,7 @@ mod tests { }, solana_pubkey::Pubkey, solana_rent::Rent, - solana_sdk_ids::{system_program, sysvar}, + solana_sdk_ids::sysvar, std::{fs::File, io::Read, ops::Range, sync::atomic::AtomicU64}, }; diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index b850756110411d..2a956ae4b4c67e 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -13385,6 +13385,312 @@ fn test_deploy_last_epoch_slot() { assert_eq!(result_with_feature_enabled, Ok(())); } +#[test] +fn test_loader_v3_to_v4_migration() { + solana_logger::setup(); + + // Bank Setup + let (mut genesis_config, mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + activate_feature( + &mut genesis_config, + solana_feature_set::enable_loader_v4::id(), + ); + let mut bank = Bank::new_for_tests(&genesis_config); + bank.activate_feature(&feature_set::remove_accounts_executable_flag_checks::id()); + let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); + let fee_calculator = genesis_config.fee_rate_governor.create_fee_calculator(); + let mut next_slot = 1; + + // deploy a program + let mut file = File::open("../programs/bpf_loader/test_elfs/out/noop_aligned.so").unwrap(); + let mut elf = Vec::new(); + file.read_to_end(&mut elf).unwrap(); + + let program_keypair = Keypair::new(); + let (programdata_address, _) = Pubkey::find_program_address( + &[program_keypair.pubkey().as_ref()], + &bpf_loader_upgradeable::id(), + ); + let payer_keypair = Keypair::new(); + let upgrade_authority_keypair = Keypair::new(); + + let min_program_balance = + bank.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()); + let mut program_account = AccountSharedData::new( + min_program_balance, + UpgradeableLoaderState::size_of_program(), + &bpf_loader_upgradeable::id(), + ); + program_account + .set_state(&UpgradeableLoaderState::Program { + programdata_address, + }) + .unwrap(); + bank.store_account(&program_keypair.pubkey(), &program_account); + + let closed_programdata_account = AccountSharedData::new(0, 0, &bpf_loader_upgradeable::id()); + + let mut uninitialized_programdata_account = AccountSharedData::new( + 0, + UpgradeableLoaderState::size_of_programdata_metadata(), + &bpf_loader_upgradeable::id(), + ); + uninitialized_programdata_account + .set_state(&UpgradeableLoaderState::Uninitialized) + .unwrap(); + + let mut finalized_programdata_account = AccountSharedData::new( + 0, + UpgradeableLoaderState::size_of_programdata(elf.len()), + &bpf_loader_upgradeable::id(), + ); + finalized_programdata_account + .set_state(&UpgradeableLoaderState::ProgramData { + slot: 0, + upgrade_authority_address: None, + }) + .unwrap(); + finalized_programdata_account + .data_as_mut_slice() + .get_mut(UpgradeableLoaderState::size_of_programdata_metadata()..) + .unwrap() + .copy_from_slice(&elf); + let message = Message::new( + &[bpf_loader_upgradeable::migrate_program( + &programdata_address, + &program_keypair.pubkey(), + &program_keypair.pubkey(), + )], + Some(&payer_keypair.pubkey()), + ); + let signers = &[&payer_keypair, &program_keypair]; + let finalized_migration_transaction = + Transaction::new(signers, message.clone(), bank.last_blockhash()); + + let mut upgradeable_programdata_account = AccountSharedData::new( + 0, + UpgradeableLoaderState::size_of_programdata(elf.len()), + &bpf_loader_upgradeable::id(), + ); + let min_programdata_balance = + bank.get_minimum_balance_for_rent_exemption(upgradeable_programdata_account.data().len()); + upgradeable_programdata_account.set_lamports(min_programdata_balance); + upgradeable_programdata_account + .set_state(&UpgradeableLoaderState::ProgramData { + slot: 0, + upgrade_authority_address: Some(upgrade_authority_keypair.pubkey()), + }) + .unwrap(); + upgradeable_programdata_account + .data_as_mut_slice() + .get_mut(UpgradeableLoaderState::size_of_programdata_metadata()..) + .unwrap() + .copy_from_slice(&elf); + let message = Message::new( + &[bpf_loader_upgradeable::migrate_program( + &programdata_address, + &program_keypair.pubkey(), + &upgrade_authority_keypair.pubkey(), + )], + Some(&payer_keypair.pubkey()), + ); + let signers = &[&payer_keypair, &upgrade_authority_keypair]; + let upgradeable_migration_transaction = + Transaction::new(signers, message.clone(), bank.last_blockhash()); + + let payer_account = AccountSharedData::new(LAMPORTS_PER_SOL, 0, &system_program::id()); + bank.store_account( + &programdata_address, + &upgradeable_programdata_account.clone(), + ); + bank.store_account(&payer_keypair.pubkey(), &payer_account); + + // Error case: Program was deployed in this block already + let case_redeployment_cooldown = vec![ + AccountMeta::new(programdata_address, false), + AccountMeta::new(programdata_address, false), + AccountMeta::new(programdata_address, false), + ]; + let message = Message::new( + &[Instruction::new_with_bincode( + bpf_loader_upgradeable::id(), + &UpgradeableLoaderInstruction::Migrate, + case_redeployment_cooldown, + )], + Some(&payer_keypair.pubkey()), + ); + let signers = &[&payer_keypair]; + let transaction = Transaction::new(signers, message.clone(), bank.last_blockhash()); + let error = bank.process_transaction(&transaction).unwrap_err(); + assert_eq!( + error, + TransactionError::InstructionError(0, InstructionError::InvalidArgument) + ); + + let bank = new_bank_from_parent_with_bank_forks( + &bank_forks, + bank.clone(), + &Pubkey::default(), + next_slot, + ); + next_slot += 1; + + // All other error cases + let case_too_few_accounts = vec![ + AccountMeta::new(programdata_address, false), + AccountMeta::new_readonly(upgrade_authority_keypair.pubkey(), true), + ]; + let case_readonly_programdata = vec![ + AccountMeta::new_readonly(programdata_address, false), + AccountMeta::new(program_keypair.pubkey(), false), + AccountMeta::new_readonly(upgrade_authority_keypair.pubkey(), true), + ]; + let case_incorrect_authority = vec![ + AccountMeta::new(programdata_address, false), + AccountMeta::new_readonly(upgrade_authority_keypair.pubkey(), true), + AccountMeta::new(program_keypair.pubkey(), false), + ]; + let case_missing_signature = vec![ + AccountMeta::new(programdata_address, false), + AccountMeta::new(program_keypair.pubkey(), false), + AccountMeta::new_readonly(upgrade_authority_keypair.pubkey(), false), + ]; + let case_readonly_program = vec![ + AccountMeta::new(programdata_address, false), + AccountMeta::new_readonly(program_keypair.pubkey(), false), + AccountMeta::new_readonly(upgrade_authority_keypair.pubkey(), true), + ]; + let case_program_has_wrong_owner = vec![ + AccountMeta::new(programdata_address, false), + AccountMeta::new(upgrade_authority_keypair.pubkey(), false), + AccountMeta::new_readonly(upgrade_authority_keypair.pubkey(), true), + ]; + let case_incorrect_programdata_address = vec![ + AccountMeta::new(program_keypair.pubkey(), false), + AccountMeta::new(program_keypair.pubkey(), false), + AccountMeta::new_readonly(program_keypair.pubkey(), true), + AccountMeta::new_readonly(upgrade_authority_keypair.pubkey(), true), + ]; + let case_invalid_program_account = vec![ + AccountMeta::new(programdata_address, false), + AccountMeta::new(programdata_address, false), + AccountMeta::new_readonly(upgrade_authority_keypair.pubkey(), true), + ]; + let case_missing_loader_v4 = vec![ + AccountMeta::new(programdata_address, false), + AccountMeta::new(program_keypair.pubkey(), false), + AccountMeta::new_readonly(upgrade_authority_keypair.pubkey(), true), + ]; + for (instruction_accounts, expected_error) in [ + ( + case_too_few_accounts, + InstructionError::NotEnoughAccountKeys, + ), + (case_readonly_programdata, InstructionError::InvalidArgument), + ( + case_incorrect_authority, + InstructionError::IncorrectAuthority, + ), + ( + case_missing_signature, + InstructionError::MissingRequiredSignature, + ), + (case_readonly_program, InstructionError::InvalidArgument), + ( + case_program_has_wrong_owner, + InstructionError::IncorrectProgramId, + ), + ( + case_incorrect_programdata_address, + InstructionError::InvalidArgument, + ), + ( + case_invalid_program_account, + InstructionError::InvalidAccountData, + ), + (case_missing_loader_v4, InstructionError::MissingAccount), + ] { + let message = Message::new( + &[Instruction::new_with_bincode( + bpf_loader_upgradeable::id(), + &UpgradeableLoaderInstruction::Migrate, + instruction_accounts, + )], + Some(&payer_keypair.pubkey()), + ); + let signers = &[&payer_keypair, &upgrade_authority_keypair, &program_keypair]; + let transaction = Transaction::new( + &signers[..message.header.num_required_signatures as usize], + message.clone(), + bank.last_blockhash(), + ); + let error = bank.process_transaction(&transaction).unwrap_err(); + assert_eq!(error, TransactionError::InstructionError(0, expected_error)); + } + + for (mut programdata_account, transaction, expected_execution_result) in [ + ( + closed_programdata_account, + finalized_migration_transaction.clone(), + Err(TransactionError::InstructionError( + 0, + InstructionError::InvalidInstructionData, + )), + ), + ( + uninitialized_programdata_account, + finalized_migration_transaction.clone(), + Err(TransactionError::InstructionError( + 0, + InstructionError::InvalidInstructionData, + )), + ), + ( + finalized_programdata_account, + finalized_migration_transaction, + Ok(()), + ), + ( + upgradeable_programdata_account, + upgradeable_migration_transaction, + Ok(()), + ), + ] { + let bank = new_bank_from_parent_with_bank_forks( + &bank_forks, + bank.clone(), + &Pubkey::default(), + next_slot, + ); + next_slot += 1; + + let min_programdata_balance = + bank.get_minimum_balance_for_rent_exemption(programdata_account.data().len()); + programdata_account.set_lamports(min_programdata_balance); + let payer_balance = min_program_balance + .saturating_add(min_programdata_balance) + .saturating_add(LAMPORTS_PER_SOL) + .saturating_add(fee_calculator.lamports_per_signature); + let payer_account = AccountSharedData::new(payer_balance, 0, &system_program::id()); + bank.store_account(&programdata_address, &programdata_account); + bank.store_account(&payer_keypair.pubkey(), &payer_account); + let result = bank.process_transaction(&transaction); + assert!(result.is_ok(), "result: {:?}", result); + + goto_end_of_slot(bank.clone()); + let bank = + new_bank_from_parent_with_bank_forks(&bank_forks, bank, &Pubkey::default(), next_slot); + next_slot += 1; + + let instruction = Instruction::new_with_bytes(program_keypair.pubkey(), &[], Vec::new()); + let message = Message::new(&[instruction], Some(&mint_keypair.pubkey())); + let binding = mint_keypair.insecure_clone(); + let transaction = Transaction::new(&[&binding], message, bank.last_blockhash()); + let execution_result = bank.process_transaction(&transaction); + assert_eq!(execution_result, expected_execution_result); + } +} + #[test] fn test_blockhash_last_valid_block_height() { let genesis_config = GenesisConfig::default(); diff --git a/sdk/loader-v3-interface/src/instruction.rs b/sdk/loader-v3-interface/src/instruction.rs index 3a970df9964f03..120b12bf2fe224 100644 --- a/sdk/loader-v3-interface/src/instruction.rs +++ b/sdk/loader-v3-interface/src/instruction.rs @@ -5,7 +5,7 @@ use { crate::{get_program_data_address, state::UpgradeableLoaderState}, solana_instruction::{error::InstructionError, AccountMeta, Instruction}, solana_pubkey::Pubkey, - solana_sdk_ids::{bpf_loader_upgradeable::id, sysvar}, + solana_sdk_ids::{bpf_loader_upgradeable::id, loader_v4, sysvar}, solana_system_interface::instruction as system_instruction, }; @@ -171,6 +171,14 @@ pub enum UpgradeableLoaderInstruction { /// 1. `[signer]` The current authority. /// 2. `[signer]` The new authority. SetAuthorityChecked, + + /// Migrate the program to loader-v4. + /// + /// # Account references + /// 0. `[writable]` The ProgramData account. + /// 1. `[writable]` The Program account. + /// 2. `[signer]` The current authority. + Migrate, } #[cfg(feature = "bincode")] @@ -299,6 +307,10 @@ pub fn is_set_authority_checked_instruction(instruction_data: &[u8]) -> bool { !instruction_data.is_empty() && 7 == instruction_data[0] } +pub fn is_migrate_instruction(instruction_data: &[u8]) -> bool { + !instruction_data.is_empty() && 8 == instruction_data[0] +} + #[cfg(feature = "bincode")] /// Returns the instructions required to set a buffers's authority. pub fn set_buffer_authority( @@ -440,6 +452,23 @@ pub fn extend_program( ) } +/// Returns the instructions required to migrate a program to loader-v4. +#[cfg(feature = "bincode")] +pub fn migrate_program( + programdata_address: &Pubkey, + program_address: &Pubkey, + authority: &Pubkey, +) -> Instruction { + let accounts = vec![ + AccountMeta::new(*programdata_address, false), + AccountMeta::new(*program_address, false), + AccountMeta::new_readonly(*authority, true), + AccountMeta::new_readonly(loader_v4::id(), false), + ]; + + Instruction::new_with_bincode(id(), &UpgradeableLoaderInstruction::Migrate, accounts) +} + #[cfg(test)] mod tests { use super::*; @@ -533,4 +562,13 @@ mod tests { UpgradeableLoaderInstruction::Upgrade {}, ); } + + #[test] + fn test_is_migrate_instruction() { + assert!(!is_migrate_instruction(&[])); + assert_is_instruction( + is_migrate_instruction, + UpgradeableLoaderInstruction::Migrate {}, + ); + } } diff --git a/sdk/program/src/bpf_loader_upgradeable.rs b/sdk/program/src/bpf_loader_upgradeable.rs index eb898f53ddff60..c5b898697b75f8 100644 --- a/sdk/program/src/bpf_loader_upgradeable.rs +++ b/sdk/program/src/bpf_loader_upgradeable.rs @@ -5,8 +5,9 @@ pub use solana_loader_v3_interface::{ instruction::{ close, close_any, create_buffer, deploy_with_max_program_len, extend_program, is_close_instruction, is_set_authority_checked_instruction, is_set_authority_instruction, - is_upgrade_instruction, set_buffer_authority, set_buffer_authority_checked, - set_upgrade_authority, set_upgrade_authority_checked, upgrade, write, + is_upgrade_instruction, migrate_program, set_buffer_authority, + set_buffer_authority_checked, set_upgrade_authority, set_upgrade_authority_checked, + upgrade, write, }, state::UpgradeableLoaderState, }; diff --git a/transaction-status/src/parse_bpf_loader.rs b/transaction-status/src/parse_bpf_loader.rs index 33df60eba104ba..74c5027bf5528e 100644 --- a/transaction-status/src/parse_bpf_loader.rs +++ b/transaction-status/src/parse_bpf_loader.rs @@ -187,6 +187,17 @@ pub fn parse_bpf_upgradeable_loader( }), }) } + UpgradeableLoaderInstruction::Migrate => { + check_num_bpf_upgradeable_loader_accounts(&instruction.accounts, 3)?; + Ok(ParsedInstructionEnum { + instruction_type: "migrate".to_string(), + info: json!({ + "programDataAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "programAccount": account_keys[instruction.accounts[1] as usize].to_string(), + "authority": account_keys[instruction.accounts[2] as usize].to_string(), + }), + }) + } } }