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/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(), + }), + }) + } } }