diff --git a/Cargo.lock b/Cargo.lock index 3e06643495..4dec0434af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5856,6 +5856,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "solana-loader-v3-program" +version = "1.16.0" +dependencies = [ + "bincode", + "log", + "rand 0.7.3", + "solana-measure", + "solana-program-runtime", + "solana-sdk 1.16.0", + "solana_rbpf", +] + [[package]] name = "solana-local-cluster" version = "1.16.0" diff --git a/Cargo.toml b/Cargo.toml index 1ba00981b7..5bf2ffb82e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ members = [ "programs/compute-budget", "programs/config", "programs/ed25519-tests", + "programs/loader-v3", "programs/stake", "programs/vote", "programs/zk-token-proof", diff --git a/program-runtime/src/loaded_programs.rs b/program-runtime/src/loaded_programs.rs index 2c495fc76e..9866c73ed0 100644 --- a/program-runtime/src/loaded_programs.rs +++ b/program-runtime/src/loaded_programs.rs @@ -9,8 +9,8 @@ use { vm::{BuiltInProgram, VerifiedExecutable}, }, solana_sdk::{ - bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, clock::Slot, pubkey::Pubkey, - saturating_add_assign, + bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, clock::Slot, loader_v3, + pubkey::Pubkey, saturating_add_assign, }, std::{ collections::HashMap, @@ -61,7 +61,7 @@ pub enum LoadedProgramType { Invalid, LegacyV0(VerifiedExecutable>), LegacyV1(VerifiedExecutable>), - // Typed(TypedProgram>), + Typed(VerifiedExecutable>), BuiltIn(BuiltInProgram>), } @@ -71,6 +71,7 @@ impl Debug for LoadedProgramType { LoadedProgramType::Invalid => write!(f, "LoadedProgramType::Invalid"), LoadedProgramType::LegacyV0(_) => write!(f, "LoadedProgramType::LegacyV0"), LoadedProgramType::LegacyV1(_) => write!(f, "LoadedProgramType::LegacyV1"), + LoadedProgramType::Typed(_) => write!(f, "LoadedProgramType::Typed"), LoadedProgramType::BuiltIn(_) => write!(f, "LoadedProgramType::BuiltIn"), } } @@ -143,6 +144,8 @@ impl LoadedProgram { LoadedProgramType::LegacyV0(VerifiedExecutable::from_executable(executable)?) } else if bpf_loader::check_id(loader_key) || bpf_loader_upgradeable::check_id(loader_key) { LoadedProgramType::LegacyV1(VerifiedExecutable::from_executable(executable)?) + } else if loader_v3::check_id(loader_key) { + LoadedProgramType::Typed(VerifiedExecutable::from_executable(executable)?) } else { panic!(); }; @@ -156,6 +159,7 @@ impl LoadedProgram { match &mut program { LoadedProgramType::LegacyV0(executable) => executable.jit_compile(), LoadedProgramType::LegacyV1(executable) => executable.jit_compile(), + LoadedProgramType::Typed(executable) => executable.jit_compile(), _ => Err(EbpfError::JitNotCompiled), }?; jit_compile_time.stop(); diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index f77461cac3..9a7680b719 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -578,7 +578,7 @@ fn process_instruction_common( LoadedProgramType::Invalid => Err(InstructionError::InvalidAccountData), LoadedProgramType::LegacyV0(executable) => execute(executable, invoke_context), LoadedProgramType::LegacyV1(executable) => execute(executable, invoke_context), - LoadedProgramType::BuiltIn(_) => Err(InstructionError::IncorrectProgramId), + _ => Err(InstructionError::IncorrectProgramId), } } diff --git a/programs/loader-v3/Cargo.toml b/programs/loader-v3/Cargo.toml new file mode 100644 index 0000000000..26bebbbacf --- /dev/null +++ b/programs/loader-v3/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "solana-loader-v3-program" +version = "1.16.0" +description = "Solana Loader v3" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2021" +publish = false + +[dependencies] +log = { workspace = true } +rand = { workspace = true } +solana-measure = { workspace = true } +solana-program-runtime = { workspace = true } +solana-sdk = { workspace = true } +solana_rbpf = { workspace = true } + +[dev-dependencies] +bincode = { workspace = true } + +[lib] +crate-type = ["lib"] +name = "solana_loader_v3_program" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/programs/loader-v3/src/lib.rs b/programs/loader-v3/src/lib.rs new file mode 100644 index 0000000000..41cb683517 --- /dev/null +++ b/programs/loader-v3/src/lib.rs @@ -0,0 +1,1472 @@ +use { + rand::Rng, + solana_measure::measure::Measure, + solana_program_runtime::{ + compute_budget::ComputeBudget, + ic_logger_msg, + invoke_context::InvokeContext, + loaded_programs::{LoadProgramMetrics, LoadedProgram, LoadedProgramType}, + log_collector::LogCollector, + stable_log, + }, + solana_rbpf::{ + aligned_memory::AlignedMemory, + ebpf, + memory_region::{MemoryMapping, MemoryRegion}, + verifier::RequisiteVerifier, + vm::{ + BuiltInProgram, Config, ContextObject, EbpfVm, ProgramResult, VerifiedExecutable, + PROGRAM_ENVIRONMENT_KEY_SHIFT, + }, + }, + solana_sdk::{ + entrypoint::{HEAP_LENGTH, SUCCESS}, + feature_set::FeatureSet, + instruction::InstructionError, + loader_v3::{self, LoaderV3State, DEPLOYMENT_COOLDOWN_IN_SLOTS}, + loader_v3_instruction::LoaderV3Instruction, + program_utils::limited_deserialize, + pubkey::Pubkey, + saturating_add_assign, + transaction_context::{BorrowedAccount, InstructionContext}, + }, + std::{cell::RefCell, rc::Rc, sync::Arc}, +}; + +fn get_state(data: &[u8]) -> Result<&LoaderV3State, InstructionError> { + unsafe { + let data = data + .get(0..LoaderV3State::program_data_offset()) + .ok_or(InstructionError::AccountDataTooSmall)? + .try_into() + .unwrap(); + Ok(std::mem::transmute::< + &[u8; LoaderV3State::program_data_offset()], + &LoaderV3State, + >(data)) + } +} + +fn get_state_mut(data: &mut [u8]) -> Result<&mut LoaderV3State, InstructionError> { + unsafe { + let data = data + .get_mut(0..LoaderV3State::program_data_offset()) + .ok_or(InstructionError::AccountDataTooSmall)? + .try_into() + .unwrap(); + Ok(std::mem::transmute::< + &mut [u8; LoaderV3State::program_data_offset()], + &mut LoaderV3State, + >(data)) + } +} + +pub fn load_program_from_account( + _feature_set: &FeatureSet, + compute_budget: &ComputeBudget, + log_collector: Option>>, + program: &BorrowedAccount, + use_jit: bool, + debugging_features: bool, +) -> Result<(Arc, LoadProgramMetrics), InstructionError> { + let mut load_program_metrics = LoadProgramMetrics { + program_id: program.get_key().to_string(), + ..LoadProgramMetrics::default() + }; + let config = Config { + max_call_depth: compute_budget.max_call_depth, + stack_frame_size: compute_budget.stack_frame_size, + enable_stack_frame_gaps: false, + instruction_meter_checkpoint_distance: 10000, + enable_instruction_meter: true, + enable_instruction_tracing: debugging_features, + enable_symbol_and_section_labels: debugging_features, + reject_broken_elfs: true, + noop_instruction_rate: 256, + sanitize_user_provided_values: true, + runtime_environment_key: rand::thread_rng() + .gen::() + .checked_shr(PROGRAM_ENVIRONMENT_KEY_SHIFT) + .unwrap_or(0), + external_internal_function_hash_collision: true, + reject_callx_r10: true, + dynamic_stack_frames: true, + enable_sdiv: true, + optimize_rodata: true, + static_syscalls: true, + enable_elf_vaddr: true, + reject_rodata_stack_overlap: true, + new_elf_parser: true, + aligned_memory_mapping: true, + // Warning, do not use `Config::default()` so that configuration here is explicit. + }; + let loader = BuiltInProgram::new_loader(config); + let state = get_state(program.get_data())?; + let programdata = program + .get_data() + .get(LoaderV3State::program_data_offset()..) + .ok_or(InstructionError::AccountDataTooSmall)?; + let loaded_program = LoadedProgram::new( + &loader_v3::id(), + Arc::new(loader), + state.slot, + programdata, + program.get_data().len(), + use_jit, + &mut load_program_metrics, + ) + .map_err(|err| { + ic_logger_msg!(log_collector, "{}", err); + InstructionError::InvalidAccountData + })?; + Ok((Arc::new(loaded_program), load_program_metrics)) +} + +fn calculate_heap_cost(heap_size: u64, heap_cost: u64) -> u64 { + const KIBIBYTE: u64 = 1024; + const PAGE_SIZE_KB: u64 = 32; + heap_size + .saturating_add(PAGE_SIZE_KB.saturating_mul(KIBIBYTE).saturating_sub(1)) + .saturating_div(PAGE_SIZE_KB.saturating_mul(KIBIBYTE)) + .saturating_sub(1) + .saturating_mul(heap_cost) +} + +/// Create the SBF virtual machine +pub fn create_vm<'a, 'b>( + invoke_context: &'a mut InvokeContext<'b>, + program: &'a VerifiedExecutable>, +) -> Result>, InstructionError> { + let config = program.get_executable().get_config(); + let compute_budget = invoke_context.get_compute_budget(); + let heap_size = compute_budget.heap_size.unwrap_or(HEAP_LENGTH); + invoke_context.consume_checked(calculate_heap_cost( + heap_size as u64, + compute_budget.heap_cost, + ))?; + let mut stack = AlignedMemory::<{ ebpf::HOST_ALIGN }>::zero_filled(config.stack_size()); + let mut heap = AlignedMemory::<{ ebpf::HOST_ALIGN }>::zero_filled( + compute_budget.heap_size.unwrap_or(HEAP_LENGTH), + ); + let stack_len = stack.len(); + let regions: Vec = vec![ + program.get_executable().get_ro_region(), + MemoryRegion::new_writable_gapped(stack.as_slice_mut(), ebpf::MM_STACK_START, 0), + MemoryRegion::new_writable(heap.as_slice_mut(), ebpf::MM_HEAP_START), + ]; + let log_collector = invoke_context.get_log_collector(); + MemoryMapping::new(regions, config) + .and_then(|memory_mapping| EbpfVm::new(program, invoke_context, memory_mapping, stack_len)) + .map_err(|err| { + ic_logger_msg!(log_collector, "Failed to create SBF VM: {}", err); + InstructionError::ProgramEnvironmentSetupFailure + }) +} + +fn execute( + invoke_context: &mut InvokeContext, + program: &VerifiedExecutable>, +) -> Result<(), InstructionError> { + let log_collector = invoke_context.get_log_collector(); + let stack_height = invoke_context.get_stack_height(); + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let program_id = *instruction_context.get_last_program_key(transaction_context)?; + #[cfg(any(target_os = "windows", not(target_arch = "x86_64")))] + let use_jit = false; + #[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))] + let use_jit = program.get_executable().get_compiled_program().is_some(); + + let compute_meter_prev = invoke_context.get_remaining(); + let mut create_vm_time = Measure::start("create_vm"); + let mut vm = create_vm( + invoke_context, + // We dropped the lifetime tracking in the Executor by setting it to 'static, + // thus we need to reintroduce the correct lifetime of InvokeContext here again. + unsafe { std::mem::transmute(program) }, + )?; + create_vm_time.stop(); + + let mut execute_time = Measure::start("execute"); + stable_log::program_invoke(&log_collector, &program_id, stack_height); + let (compute_units_consumed, result) = vm.execute_program(!use_jit); + drop(vm); + ic_logger_msg!( + log_collector, + "Program {} consumed {} of {} compute units", + &program_id, + compute_units_consumed, + compute_meter_prev + ); + execute_time.stop(); + + let timings = &mut invoke_context.timings; + timings.create_vm_us = timings.create_vm_us.saturating_add(create_vm_time.as_us()); + timings.execute_us = timings.execute_us.saturating_add(execute_time.as_us()); + + match result { + ProgramResult::Ok(status) if status != SUCCESS => { + let error = status.into(); + Err(error) + } + ProgramResult::Err(_) => Err(InstructionError::ProgramFailedToComplete), + _ => Ok(()), + } +} + +fn check_program_account( + log_collector: &Option>>, + instruction_context: &InstructionContext, + program: &BorrowedAccount, + authority_address: &Pubkey, +) -> Result { + if !loader_v3::check_id(program.get_owner()) { + ic_logger_msg!(log_collector, "Program not owned by loader"); + return Err(InstructionError::InvalidAccountOwner); + } + if program.get_data().is_empty() { + ic_logger_msg!(log_collector, "Program is uninitialized"); + return Err(InstructionError::InvalidAccountData); + } + let state = get_state(program.get_data())?; + if !program.is_writable() { + ic_logger_msg!(log_collector, "Program is not writeable"); + return Err(InstructionError::InvalidArgument); + } + if !instruction_context.is_instruction_account_signer(1)? { + ic_logger_msg!(log_collector, "Authority did not sign"); + return Err(InstructionError::MissingRequiredSignature); + } + if state.authority_address.is_none() { + ic_logger_msg!(log_collector, "Program is finalized"); + return Err(InstructionError::Immutable); + } + if state.authority_address != Some(*authority_address) { + ic_logger_msg!(log_collector, "Incorrect authority provided"); + return Err(InstructionError::IncorrectAuthority); + } + Ok(*state) +} + +pub fn process_instruction_write( + invoke_context: &mut InvokeContext, + offset: u32, + bytes: Vec, +) -> Result<(), InstructionError> { + let log_collector = invoke_context.get_log_collector(); + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let mut program = instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + let authority_address = instruction_context + .get_index_of_instruction_account_in_transaction(1) + .and_then(|index| transaction_context.get_key_of_account_at_index(index))?; + let mut payer = instruction_context + .try_borrow_instruction_account(transaction_context, 2) + .ok(); + let is_initialization = offset == 0 && program.get_data().is_empty(); + if is_initialization { + if !loader_v3::check_id(program.get_owner()) { + ic_logger_msg!(log_collector, "Program not owned by loader"); + return Err(InstructionError::InvalidAccountOwner); + } + if !program.is_writable() { + ic_logger_msg!(log_collector, "Program is not writeable"); + return Err(InstructionError::InvalidArgument); + } + if !instruction_context.is_instruction_account_signer(1)? { + ic_logger_msg!(log_collector, "Authority did not sign"); + return Err(InstructionError::MissingRequiredSignature); + } + } else { + let state = check_program_account( + &log_collector, + instruction_context, + &program, + authority_address, + )?; + if state.is_deployed { + ic_logger_msg!(log_collector, "Program is not retracted"); + return Err(InstructionError::InvalidArgument); + } + } + if payer.is_some() && !instruction_context.is_instruction_account_signer(2)? { + ic_logger_msg!(log_collector, "Payer did not sign"); + return Err(InstructionError::MissingRequiredSignature); + } + if payer.is_some() && !instruction_context.is_instruction_account_writable(2)? { + ic_logger_msg!(log_collector, "Payer is not writeable"); + return Err(InstructionError::InvalidArgument); + } + let program_size = program + .get_data() + .len() + .saturating_sub(LoaderV3State::program_data_offset()); + if offset as usize > program_size { + ic_logger_msg!(log_collector, "Write out of bounds"); + return Err(InstructionError::AccountDataTooSmall); + } + let end_offset = (offset as usize).saturating_add(bytes.len()); + let rent = invoke_context.get_sysvar_cache().get_rent()?; + let required_lamports = + rent.minimum_balance(LoaderV3State::program_data_offset().saturating_add(end_offset)); + let transfer_lamports = required_lamports.saturating_sub(program.get_lamports()); + if transfer_lamports > 0 { + payer = payer.filter(|payer| payer.get_lamports() >= transfer_lamports); + if payer.is_none() { + ic_logger_msg!( + log_collector, + "Insufficient lamports, {} are required", + required_lamports + ); + return Err(InstructionError::InsufficientFunds); + } + } + if end_offset > program_size { + program.set_data_length(LoaderV3State::program_data_offset().saturating_add(end_offset))?; + } + if let Some(mut payer) = payer { + payer.checked_sub_lamports(transfer_lamports)?; + program.checked_add_lamports(transfer_lamports)?; + } + if is_initialization { + let state = get_state_mut(program.get_data_mut()?)?; + state.slot = invoke_context.get_sysvar_cache().get_clock()?.slot; + state.is_deployed = false; + state.authority_address = Some(*authority_address); + } + program + .get_data_mut()? + .get_mut( + LoaderV3State::program_data_offset().saturating_add(offset as usize) + ..LoaderV3State::program_data_offset().saturating_add(end_offset), + ) + .ok_or(InstructionError::AccountDataTooSmall)? + .copy_from_slice(&bytes); + Ok(()) +} + +pub fn process_instruction_truncate( + invoke_context: &mut InvokeContext, + offset: u32, +) -> Result<(), InstructionError> { + let log_collector = invoke_context.get_log_collector(); + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let mut program = instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + let authority_address = instruction_context + .get_index_of_instruction_account_in_transaction(1) + .and_then(|index| transaction_context.get_key_of_account_at_index(index))?; + let mut recipient = + instruction_context.try_borrow_instruction_account(transaction_context, 2)?; + let state = check_program_account( + &log_collector, + instruction_context, + &program, + authority_address, + )?; + if state.is_deployed { + ic_logger_msg!(log_collector, "Program is not retracted"); + return Err(InstructionError::InvalidArgument); + } + let program_size = program + .get_data() + .len() + .saturating_sub(LoaderV3State::program_data_offset()); + if offset as usize > program_size { + ic_logger_msg!(log_collector, "Truncate out of bounds"); + return Err(InstructionError::AccountDataTooSmall); + } + let required_lamports = if offset == 0 { + program.set_data_length(0)?; + 0 + } else { + program.set_data_length( + LoaderV3State::program_data_offset().saturating_add(offset as usize), + )?; + let rent = invoke_context.get_sysvar_cache().get_rent()?; + rent.minimum_balance(program.get_data().len()) + }; + let transfer_lamports = program.get_lamports().saturating_sub(required_lamports); + program.checked_sub_lamports(transfer_lamports)?; + recipient.checked_add_lamports(transfer_lamports)?; + Ok(()) +} + +pub fn process_instruction_deploy( + invoke_context: &mut InvokeContext, + use_jit: bool, +) -> Result<(), InstructionError> { + let log_collector = invoke_context.get_log_collector(); + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let mut program = instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + let authority_address = instruction_context + .get_index_of_instruction_account_in_transaction(1) + .and_then(|index| transaction_context.get_key_of_account_at_index(index))?; + let source_program = instruction_context + .try_borrow_instruction_account(transaction_context, 2) + .ok(); + let state = check_program_account( + &log_collector, + instruction_context, + &program, + authority_address, + )?; + let current_slot = invoke_context.get_sysvar_cache().get_clock()?.slot; + if state.slot.saturating_add(DEPLOYMENT_COOLDOWN_IN_SLOTS) > current_slot { + ic_logger_msg!( + log_collector, + "Program was deployed recently, cooldown still in effect" + ); + return Err(InstructionError::InvalidArgument); + } + if state.is_deployed { + ic_logger_msg!(log_collector, "Destination program is not retracted"); + return Err(InstructionError::InvalidArgument); + } + let buffer = if let Some(ref source_program) = source_program { + let source_state = check_program_account( + &log_collector, + instruction_context, + source_program, + authority_address, + )?; + if source_state.is_deployed { + ic_logger_msg!(log_collector, "Source program is not retracted"); + return Err(InstructionError::InvalidArgument); + } + source_program + } else { + &program + }; + let (_executor, load_program_metrics) = load_program_from_account( + &invoke_context.feature_set, + invoke_context.get_compute_budget(), + invoke_context.get_log_collector(), + buffer, + use_jit, + false, + )?; + load_program_metrics.submit_datapoint(&mut invoke_context.timings); + if let Some(mut source_program) = source_program { + let rent = invoke_context.get_sysvar_cache().get_rent()?; + let required_lamports = rent.minimum_balance(program.get_data().len()); + let transfer_lamports = program.get_lamports().saturating_sub(required_lamports); + program.set_data_from_slice(source_program.get_data())?; + source_program.set_data_length(0)?; + source_program.checked_sub_lamports(transfer_lamports)?; + program.checked_add_lamports(transfer_lamports)?; + } + let state = get_state_mut(program.get_data_mut()?)?; + state.slot = current_slot; + state.is_deployed = true; + Ok(()) +} + +pub fn process_instruction_retract( + invoke_context: &mut InvokeContext, +) -> Result<(), InstructionError> { + let log_collector = invoke_context.get_log_collector(); + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let mut program = instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + let authority_address = instruction_context + .get_index_of_instruction_account_in_transaction(1) + .and_then(|index| transaction_context.get_key_of_account_at_index(index))?; + let state = check_program_account( + &log_collector, + instruction_context, + &program, + authority_address, + )?; + let current_slot = invoke_context.get_sysvar_cache().get_clock()?.slot; + if state.slot.saturating_add(DEPLOYMENT_COOLDOWN_IN_SLOTS) > current_slot { + ic_logger_msg!( + log_collector, + "Program was deployed recently, cooldown still in effect" + ); + return Err(InstructionError::InvalidArgument); + } + if !state.is_deployed { + ic_logger_msg!(log_collector, "Program is not deployed"); + return Err(InstructionError::InvalidArgument); + } + let state = get_state_mut(program.get_data_mut()?)?; + state.is_deployed = false; + Ok(()) +} + +pub fn process_instruction_transfer_authority( + invoke_context: &mut InvokeContext, +) -> Result<(), InstructionError> { + let log_collector = invoke_context.get_log_collector(); + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let mut program = instruction_context.try_borrow_instruction_account(transaction_context, 0)?; + let authority_address = instruction_context + .get_index_of_instruction_account_in_transaction(1) + .and_then(|index| transaction_context.get_key_of_account_at_index(index))?; + let new_authority_address = instruction_context + .get_index_of_instruction_account_in_transaction(2) + .and_then(|index| transaction_context.get_key_of_account_at_index(index)) + .ok() + .cloned(); + let _state = check_program_account( + &log_collector, + instruction_context, + &program, + authority_address, + )?; + if new_authority_address.is_some() && !instruction_context.is_instruction_account_signer(2)? { + ic_logger_msg!(log_collector, "New authority did not sign"); + return Err(InstructionError::MissingRequiredSignature); + } + let state = get_state_mut(program.get_data_mut()?)?; + state.authority_address = new_authority_address; + Ok(()) +} + +pub fn process_instruction(invoke_context: &mut InvokeContext) -> Result<(), InstructionError> { + let use_jit = true; + let log_collector = invoke_context.get_log_collector(); + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let instruction_data = instruction_context.get_instruction_data(); + let program_id = instruction_context.get_last_program_key(transaction_context)?; + if loader_v3::check_id(program_id) { + match limited_deserialize(instruction_data)? { + LoaderV3Instruction::Write { offset, bytes } => { + process_instruction_write(invoke_context, offset, bytes) + } + LoaderV3Instruction::Truncate { offset } => { + process_instruction_truncate(invoke_context, offset) + } + LoaderV3Instruction::Deploy => process_instruction_deploy(invoke_context, use_jit), + LoaderV3Instruction::Retract => process_instruction_retract(invoke_context), + LoaderV3Instruction::TransferAuthority => { + process_instruction_transfer_authority(invoke_context) + } + } + } else { + let program = instruction_context.try_borrow_last_program_account(transaction_context)?; + if !loader_v3::check_id(program.get_owner()) { + ic_logger_msg!(log_collector, "Program not owned by loader"); + return Err(InstructionError::InvalidAccountOwner); + } + if program.get_data().is_empty() { + ic_logger_msg!(log_collector, "Program is uninitialized"); + return Err(InstructionError::InvalidAccountData); + } + let state = get_state(program.get_data())?; + if !state.is_deployed { + ic_logger_msg!(log_collector, "Program is not deployed"); + return Err(InstructionError::InvalidArgument); + } + let mut get_or_create_executor_time = Measure::start("get_or_create_executor_time"); + let (loaded_program, load_program_metrics) = load_program_from_account( + &invoke_context.feature_set, + invoke_context.get_compute_budget(), + invoke_context.get_log_collector(), + &program, + use_jit, + false, + )?; + load_program_metrics.submit_datapoint(&mut invoke_context.timings); + get_or_create_executor_time.stop(); + saturating_add_assign!( + invoke_context.timings.get_or_create_executor_us, + get_or_create_executor_time.as_us() + ); + drop(program); + match &loaded_program.program { + LoadedProgramType::Invalid => Err(InstructionError::InvalidAccountData), + LoadedProgramType::Typed(executable) => execute(invoke_context, executable), + _ => Err(InstructionError::IncorrectProgramId), + } + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_sdk::{ + account::{ + create_account_shared_data_for_test, AccountSharedData, ReadableAccount, + WritableAccount, + }, + account_utils::StateMut, + native_loader, + slot_history::Slot, + sysvar::{clock, rent}, + transaction_context::{IndexOfAccount, InstructionAccount, TransactionContext}, + }, + std::{fs::File, io::Read, path::Path}, + }; + + fn process_instruction( + mut program_indices: Vec, + instruction_data: &[u8], + mut transaction_accounts: Vec<(Pubkey, AccountSharedData)>, + instruction_accounts: &[(IndexOfAccount, bool, bool)], + expected_result: Result<(), InstructionError>, + ) -> Vec { + program_indices.insert(0, transaction_accounts.len() as IndexOfAccount); + let processor_account = AccountSharedData::new(0, 0, &native_loader::id()); + transaction_accounts.push((loader_v3::id(), processor_account)); + let compute_budget = ComputeBudget::default(); + let mut transaction_context = TransactionContext::new( + transaction_accounts, + Some(rent::Rent::default()), + compute_budget.max_invoke_stack_height, + compute_budget.max_instruction_trace_length, + ); + transaction_context.enable_cap_accounts_data_allocations_per_transaction(); + let instruction_accounts = instruction_accounts + .iter() + .enumerate() + .map( + |(instruction_account_index, (index_in_transaction, is_signer, is_writable))| { + InstructionAccount { + index_in_transaction: *index_in_transaction, + index_in_caller: *index_in_transaction, + index_in_callee: instruction_account_index as IndexOfAccount, + is_signer: *is_signer, + is_writable: *is_writable, + } + }, + ) + .collect::>(); + let mut invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); + invoke_context + .transaction_context + .get_next_instruction_context() + .unwrap() + .configure(&program_indices, &instruction_accounts, instruction_data); + let result = invoke_context + .push() + .and_then(|_| super::process_instruction(&mut invoke_context)); + let pop_result = invoke_context.pop(); + assert_eq!(result.and(pop_result), expected_result); + let mut transaction_accounts = transaction_context.deconstruct_without_keys().unwrap(); + transaction_accounts.pop(); + transaction_accounts + } + + fn load_program_account_from_elf( + is_deployed: bool, + authority_address: Option, + path: &str, + ) -> AccountSharedData { + let path = Path::new("test_elfs/out/").join(path).with_extension("so"); + let mut file = File::open(path).expect("file open failed"); + let mut elf_bytes = Vec::new(); + file.read_to_end(&mut elf_bytes).unwrap(); + let rent = rent::Rent::default(); + let account_size = + loader_v3::LoaderV3State::program_data_offset().saturating_add(elf_bytes.len()); + let mut program_account = AccountSharedData::new( + rent.minimum_balance(account_size), + account_size, + &loader_v3::id(), + ); + program_account + .set_state(&loader_v3::LoaderV3State { + slot: 0, + is_deployed, + authority_address, + }) + .unwrap(); + program_account.data_mut()[loader_v3::LoaderV3State::program_data_offset()..] + .copy_from_slice(&elf_bytes); + program_account + } + + fn clock(slot: Slot) -> AccountSharedData { + let clock = clock::Clock { + slot, + ..clock::Clock::default() + }; + create_account_shared_data_for_test(&clock) + } + + fn test_loader_instruction_general_errors(instruction: LoaderV3Instruction) { + let instruction = bincode::serialize(&instruction).unwrap(); + let authority_address = Pubkey::new_unique(); + let transaction_accounts = vec![ + ( + Pubkey::new_unique(), + load_program_account_from_elf(true, Some(authority_address), "noop"), + ), + ( + authority_address, + AccountSharedData::new(0, 0, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + load_program_account_from_elf(false, None, "noop"), + ), + ( + clock::id(), + create_account_shared_data_for_test(&clock::Clock::default()), + ), + ( + rent::id(), + create_account_shared_data_for_test(&rent::Rent::default()), + ), + ]; + + // Error: Missing program account + process_instruction( + vec![], + &instruction, + transaction_accounts.clone(), + &[], + Err(InstructionError::NotEnoughAccountKeys), + ); + + // Error: Missing authority account + process_instruction( + vec![], + &instruction, + transaction_accounts.clone(), + &[(0, false, true)], + Err(InstructionError::NotEnoughAccountKeys), + ); + + // Error: Program not owned by loader + process_instruction( + vec![], + &instruction, + transaction_accounts.clone(), + &[(1, false, true), (1, true, false), (2, true, true)], + Err(InstructionError::InvalidAccountOwner), + ); + + // Error: Program is not writeable + process_instruction( + vec![], + &instruction, + transaction_accounts.clone(), + &[(0, false, false), (1, true, false), (2, true, true)], + Err(InstructionError::InvalidArgument), + ); + + // Error: Authority did not sign + process_instruction( + vec![], + &instruction, + transaction_accounts.clone(), + &[(0, false, true), (1, false, false), (2, true, true)], + Err(InstructionError::MissingRequiredSignature), + ); + + // Error: Program is finalized + process_instruction( + vec![], + &instruction, + transaction_accounts.clone(), + &[(2, false, true), (1, true, false), (0, true, true)], + Err(InstructionError::Immutable), + ); + + // Error: Incorrect authority provided + process_instruction( + vec![], + &instruction, + transaction_accounts, + &[(0, false, true), (2, true, false), (2, true, true)], + Err(InstructionError::IncorrectAuthority), + ); + } + + #[test] + fn test_loader_instruction_write() { + let authority_address = Pubkey::new_unique(); + let mut transaction_accounts = vec![ + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &loader_v3::id()), + ), + ( + authority_address, + AccountSharedData::new(0, 0, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(10000000, 0, &loader_v3::id()), + ), + ( + Pubkey::new_unique(), + load_program_account_from_elf(true, Some(authority_address), "noop"), + ), + ( + clock::id(), + create_account_shared_data_for_test(&clock::Clock::default()), + ), + ( + rent::id(), + create_account_shared_data_for_test(&rent::Rent::default()), + ), + ]; + + // Initialize account by first write + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 0, + bytes: vec![0, 1, 2, 3], + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, true, true)], + Ok(()), + ); + assert_eq!( + accounts[0].data().len(), + loader_v3::LoaderV3State::program_data_offset().saturating_add(4), + ); + assert_eq!(accounts[0].lamports(), 1252800); + assert_eq!( + accounts[2].lamports(), + transaction_accounts[2] + .1 + .lamports() + .saturating_sub(accounts[0].lamports()), + ); + + // Error: Program is not writeable + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 0, + bytes: vec![0, 1, 2, 3], + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, false), (1, true, false), (2, true, true)], + Err(InstructionError::InvalidArgument), + ); + + // Error: Authority did not sign + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 0, + bytes: vec![0, 1, 2, 3], + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, false, false), (2, true, true)], + Err(InstructionError::MissingRequiredSignature), + ); + + // Extend account by writing at the end + transaction_accounts[0].1 = accounts[0].clone(); + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 4, + bytes: vec![4, 5, 6, 7], + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, true, true)], + Ok(()), + ); + assert_eq!( + accounts[0].data().len(), + loader_v3::LoaderV3State::program_data_offset().saturating_add(8), + ); + assert_eq!( + accounts[0].lamports(), + transaction_accounts[0].1.lamports().saturating_add(27840), + ); + assert_eq!( + accounts[2].lamports(), + transaction_accounts[2].1.lamports().saturating_sub( + accounts[0] + .lamports() + .saturating_sub(transaction_accounts[0].1.lamports()), + ), + ); + + // Overwrite existing data (no payer required) + transaction_accounts[0].1 = accounts[0].clone(); + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 2, + bytes: vec![8, 8, 8, 8], + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false)], + Ok(()), + ); + assert_eq!( + accounts[0].data().len(), + loader_v3::LoaderV3State::program_data_offset().saturating_add(8), + ); + assert_eq!(accounts[0].lamports(), transaction_accounts[0].1.lamports()); + + // Empty write + transaction_accounts[0].1 = accounts[0].clone(); + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 2, + bytes: Vec::new(), + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false)], + Ok(()), + ); + assert_eq!( + accounts[0].data().len(), + loader_v3::LoaderV3State::program_data_offset().saturating_add(8), + ); + assert_eq!(accounts[0].lamports(), transaction_accounts[0].1.lamports()); + + // Error: Program is not retracted + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 8, + bytes: vec![8, 8, 8, 8], + }) + .unwrap(), + transaction_accounts.clone(), + &[(3, false, true), (1, true, false), (2, true, true)], + Err(InstructionError::InvalidArgument), + ); + + // Error: Payer did not sign + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 8, + bytes: vec![8, 8, 8, 8], + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, false, true)], + Err(InstructionError::MissingRequiredSignature), + ); + + // Error: Payer is not writeable + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 8, + bytes: vec![8, 8, 8, 8], + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, true, false)], + Err(InstructionError::InvalidArgument), + ); + + // Error: Write out of bounds + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 9, + bytes: vec![8, 8, 8, 8], + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, true, true)], + Err(InstructionError::AccountDataTooSmall), + ); + + // Error: Insufficient funds (Bankrupt payer account) + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 8, + bytes: vec![8, 8, 8, 8], + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (1, true, true)], + Err(InstructionError::InsufficientFunds), + ); + + // Error: Insufficient funds (No payer account) + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Write { + offset: 8, + bytes: vec![8, 8, 8, 8], + }) + .unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false)], + Err(InstructionError::InsufficientFunds), + ); + + test_loader_instruction_general_errors(LoaderV3Instruction::Write { + offset: 0, + bytes: Vec::new(), + }); + } + + #[test] + fn test_loader_instruction_truncate() { + let authority_address = Pubkey::new_unique(); + let transaction_accounts = vec![ + ( + Pubkey::new_unique(), + load_program_account_from_elf(false, Some(authority_address), "noop"), + ), + ( + authority_address, + AccountSharedData::new(0, 0, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &loader_v3::id()), + ), + ( + Pubkey::new_unique(), + load_program_account_from_elf(true, Some(authority_address), "noop"), + ), + ( + clock::id(), + create_account_shared_data_for_test(&clock::Clock::default()), + ), + ( + rent::id(), + create_account_shared_data_for_test(&rent::Rent::default()), + ), + ]; + + // Cut the end off + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Truncate { offset: 4 }).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, false, true)], + Ok(()), + ); + assert_eq!( + accounts[0].data().len(), + loader_v3::LoaderV3State::program_data_offset().saturating_add(4), + ); + assert_eq!(accounts[0].lamports(), 1252800); + assert_eq!( + accounts[2].lamports(), + transaction_accounts[0] + .1 + .lamports() + .saturating_sub(accounts[0].lamports()), + ); + + // Close program account + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Truncate { offset: 0 }).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, false, true)], + Ok(()), + ); + assert_eq!(accounts[0].data().len(), 0); + assert_eq!( + accounts[2].lamports(), + transaction_accounts[0] + .1 + .lamports() + .saturating_sub(accounts[0].lamports()), + ); + + // Error: Program is uninitialized + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Truncate { offset: 0 }).unwrap(), + transaction_accounts.clone(), + &[(2, false, true), (1, true, false), (0, false, true)], + Err(InstructionError::InvalidAccountData), + ); + + // Error: Program is not retracted + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Truncate { offset: 0 }).unwrap(), + transaction_accounts.clone(), + &[(3, false, true), (1, true, false), (2, false, true)], + Err(InstructionError::InvalidArgument), + ); + + // Error: Truncate out of bounds + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Truncate { offset: 10000 }).unwrap(), + transaction_accounts, + &[(0, false, true), (1, true, false), (2, false, true)], + Err(InstructionError::AccountDataTooSmall), + ); + + test_loader_instruction_general_errors(LoaderV3Instruction::Truncate { offset: 0 }); + } + + #[test] + fn test_loader_instruction_deploy() { + let authority_address = Pubkey::new_unique(); + let mut transaction_accounts = vec![ + ( + Pubkey::new_unique(), + load_program_account_from_elf(false, Some(authority_address), "rodata"), + ), + ( + authority_address, + AccountSharedData::new(0, 0, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + load_program_account_from_elf(false, Some(authority_address), "noop"), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &loader_v3::id()), + ), + ( + Pubkey::new_unique(), + load_program_account_from_elf(false, Some(authority_address), "invalid"), + ), + (clock::id(), clock(1000)), + ( + rent::id(), + create_account_shared_data_for_test(&rent::Rent::default()), + ), + ]; + + // Deploy from its own data + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Deploy).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false)], + Ok(()), + ); + transaction_accounts[0].1 = accounts[0].clone(); + transaction_accounts[5].1 = clock(2000); + assert_eq!( + accounts[0].data().len(), + transaction_accounts[0].1.data().len(), + ); + assert_eq!(accounts[0].lamports(), transaction_accounts[0].1.lamports()); + + // Error: Source program is not writable + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Deploy).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, false, false)], + Err(InstructionError::InvalidArgument), + ); + + // Error: Source program is not retracted + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Deploy).unwrap(), + transaction_accounts.clone(), + &[(2, false, true), (1, true, false), (0, false, true)], + Err(InstructionError::InvalidArgument), + ); + + // Redeploy: Retract, then replace data by other source + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Retract).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false)], + Ok(()), + ); + transaction_accounts[0].1 = accounts[0].clone(); + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Deploy).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, false, true)], + Ok(()), + ); + transaction_accounts[0].1 = accounts[0].clone(); + assert_eq!( + accounts[0].data().len(), + transaction_accounts[2].1.data().len(), + ); + assert_eq!(accounts[2].data().len(), 0,); + assert_eq!( + accounts[2].lamports(), + transaction_accounts[2].1.lamports().saturating_sub( + accounts[0] + .lamports() + .saturating_sub(transaction_accounts[0].1.lamports()) + ), + ); + + // Error: Program was deployed recently, cooldown still in effect + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Deploy).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false)], + Err(InstructionError::InvalidArgument), + ); + transaction_accounts[5].1 = clock(3000); + + // Error: Program is uninitialized + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Deploy).unwrap(), + transaction_accounts.clone(), + &[(3, false, true), (1, true, false)], + Err(InstructionError::InvalidAccountData), + ); + + // Error: Program fails verification + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Deploy).unwrap(), + transaction_accounts.clone(), + &[(4, false, true), (1, true, false)], + Err(InstructionError::InvalidAccountData), + ); + + // Error: Program is deployed already + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Deploy).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false)], + Err(InstructionError::InvalidArgument), + ); + + test_loader_instruction_general_errors(LoaderV3Instruction::Deploy); + } + + #[test] + fn test_loader_instruction_retract() { + let authority_address = Pubkey::new_unique(); + let mut transaction_accounts = vec![ + ( + Pubkey::new_unique(), + load_program_account_from_elf(true, Some(authority_address), "rodata"), + ), + ( + authority_address, + AccountSharedData::new(0, 0, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &loader_v3::id()), + ), + ( + Pubkey::new_unique(), + load_program_account_from_elf(false, Some(authority_address), "rodata"), + ), + (clock::id(), clock(1000)), + ( + rent::id(), + create_account_shared_data_for_test(&rent::Rent::default()), + ), + ]; + + // Retract program + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Retract).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false)], + Ok(()), + ); + assert_eq!( + accounts[0].data().len(), + transaction_accounts[0].1.data().len(), + ); + assert_eq!(accounts[0].lamports(), transaction_accounts[0].1.lamports()); + + // Error: Program is uninitialized + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Retract).unwrap(), + transaction_accounts.clone(), + &[(2, false, true), (1, true, false)], + Err(InstructionError::InvalidAccountData), + ); + + // Error: Program is not deployed + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Retract).unwrap(), + transaction_accounts.clone(), + &[(3, false, true), (1, true, false)], + Err(InstructionError::InvalidArgument), + ); + + // Error: Program was deployed recently, cooldown still in effect + transaction_accounts[4].1 = clock(0); + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::Retract).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false)], + Err(InstructionError::InvalidArgument), + ); + + test_loader_instruction_general_errors(LoaderV3Instruction::Retract); + } + + #[test] + fn test_loader_instruction_transfer_authority() { + let authority_address = Pubkey::new_unique(); + let transaction_accounts = vec![ + ( + Pubkey::new_unique(), + load_program_account_from_elf(true, Some(authority_address), "rodata"), + ), + ( + authority_address, + AccountSharedData::new(0, 0, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &loader_v3::id()), + ), + ( + clock::id(), + create_account_shared_data_for_test(&clock::Clock::default()), + ), + ( + rent::id(), + create_account_shared_data_for_test(&rent::Rent::default()), + ), + ]; + + // Transfer authority + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::TransferAuthority).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false), (2, true, false)], + Ok(()), + ); + assert_eq!( + accounts[0].data().len(), + transaction_accounts[0].1.data().len(), + ); + assert_eq!(accounts[0].lamports(), transaction_accounts[0].1.lamports()); + + // Finalize program + let accounts = process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::TransferAuthority).unwrap(), + transaction_accounts.clone(), + &[(0, false, true), (1, true, false)], + Ok(()), + ); + assert_eq!( + accounts[0].data().len(), + transaction_accounts[0].1.data().len(), + ); + assert_eq!(accounts[0].lamports(), transaction_accounts[0].1.lamports()); + + // Error: Program is uninitialized + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::TransferAuthority).unwrap(), + transaction_accounts.clone(), + &[(3, false, true), (1, true, false), (2, true, false)], + Err(InstructionError::InvalidAccountData), + ); + + // Error: New authority did not sign + process_instruction( + vec![], + &bincode::serialize(&LoaderV3Instruction::TransferAuthority).unwrap(), + transaction_accounts, + &[(0, false, true), (1, true, false), (2, false, false)], + Err(InstructionError::MissingRequiredSignature), + ); + + test_loader_instruction_general_errors(LoaderV3Instruction::TransferAuthority); + } + + #[test] + fn test_execute_program() { + let program_address = Pubkey::new_unique(); + let transaction_accounts = vec![ + ( + program_address, + load_program_account_from_elf(true, None, "rodata"), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(10000000, 32, &program_address), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &loader_v3::id()), + ), + ( + Pubkey::new_unique(), + load_program_account_from_elf(false, None, "rodata"), + ), + ( + program_address, + load_program_account_from_elf(true, None, "invalid"), + ), + ]; + + // Execute program + process_instruction( + vec![0], + &[0, 1, 2, 3], + transaction_accounts.clone(), + &[(1, false, true)], + Err(InstructionError::Custom(42)), + ); + + // Error: Program not owned by loader + process_instruction( + vec![1], + &[0, 1, 2, 3], + transaction_accounts.clone(), + &[(1, false, true)], + Err(InstructionError::InvalidAccountOwner), + ); + + // Error: Program is uninitialized + process_instruction( + vec![2], + &[0, 1, 2, 3], + transaction_accounts.clone(), + &[(1, false, true)], + Err(InstructionError::InvalidAccountData), + ); + + // Error: Program is not deployed + process_instruction( + vec![3], + &[0, 1, 2, 3], + transaction_accounts.clone(), + &[(1, false, true)], + Err(InstructionError::InvalidArgument), + ); + + // Error: Program fails verification + process_instruction( + vec![4], + &[0, 1, 2, 3], + transaction_accounts, + &[(1, false, true)], + Err(InstructionError::InvalidAccountData), + ); + } +} diff --git a/programs/loader-v3/test_elfs/out/invalid.so b/programs/loader-v3/test_elfs/out/invalid.so new file mode 100644 index 0000000000..cfa6455383 Binary files /dev/null and b/programs/loader-v3/test_elfs/out/invalid.so differ diff --git a/programs/loader-v3/test_elfs/out/noop.so b/programs/loader-v3/test_elfs/out/noop.so new file mode 100755 index 0000000000..7e74e2d7a7 Binary files /dev/null and b/programs/loader-v3/test_elfs/out/noop.so differ diff --git a/programs/loader-v3/test_elfs/out/rodata.so b/programs/loader-v3/test_elfs/out/rodata.so new file mode 100755 index 0000000000..9b8a8b8ed0 Binary files /dev/null and b/programs/loader-v3/test_elfs/out/rodata.so differ diff --git a/sdk/program/src/lib.rs b/sdk/program/src/lib.rs index bff9179fb4..69d9dfbd59 100644 --- a/sdk/program/src/lib.rs +++ b/sdk/program/src/lib.rs @@ -500,6 +500,8 @@ pub mod keccak; pub mod lamports; pub mod loader_instruction; pub mod loader_upgradeable_instruction; +pub mod loader_v3; +pub mod loader_v3_instruction; pub mod log; pub mod message; pub mod native_token; diff --git a/sdk/program/src/loader_v3.rs b/sdk/program/src/loader_v3.rs new file mode 100644 index 0000000000..20331825ef --- /dev/null +++ b/sdk/program/src/loader_v3.rs @@ -0,0 +1,31 @@ +//! The v3 built-in loader program. +//! +//! This is the loader of the program runtime v2. + +use crate::pubkey::Pubkey; + +crate::declare_id!("LoaderV311111111111111111111111111111111111"); + +/// Cooldown before a program can be un-/redeployed again +pub const DEPLOYMENT_COOLDOWN_IN_SLOTS: u64 = 750; + +/// LoaderV3 account states +#[repr(C)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, AbiExample)] +pub struct LoaderV3State { + /// Slot that the program was last initialized, deployed or retracted in. + pub slot: u64, + /// True if the program is ready to be executed, false if it is retracted for maintainance. + pub is_deployed: bool, + /// Authority address, `None` means it is finalized. + pub authority_address: Option, + // The raw program data follows this serialized structure in the + // account's data. +} + +impl LoaderV3State { + /// Size of a serialized program account. + pub const fn program_data_offset() -> usize { + std::mem::size_of::() + } +} diff --git a/sdk/program/src/loader_v3_instruction.rs b/sdk/program/src/loader_v3_instruction.rs new file mode 100644 index 0000000000..6b8dcc7faf --- /dev/null +++ b/sdk/program/src/loader_v3_instruction.rs @@ -0,0 +1,74 @@ +//! Instructions for the SBF loader. + +#[repr(u8)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum LoaderV3Instruction { + /// Write ELF data into an undeployed program account. + /// + /// Writing at the end (offset is length) increases the size of the program account and + /// providing additional lamports (via the payer account) is necessary to reach rent exemption. + /// The first write (at offset zero when the account is empty) automatically + /// initializes the program account and sets the authority needed for subsequent writes. + /// Thus, the first write should be in the same transaction as the account creation. + /// + /// # Account references + /// 0. `[writable]` The program account to write to. + /// 1. `[signer]` The authority of the program. + /// 2. `[signer]` Optional, the payer account. + Write { + /// Offset at which to write the given bytes. + offset: u32, + /// Serialized program data + #[serde(with = "serde_bytes")] + bytes: Vec, + }, + + /// Decrease the size of an undeployed program account. + /// + /// Decreasing to size zero closes the program account and resets it into an uninitialized state. + /// Superflous lamports are transfered to the recipient account. + /// + /// # Account references + /// 0. `[writable]` The program account to change the size of. + /// 1. `[signer]` The authority of the program. + /// 2. `[writable]` The recipient account. + Truncate { + /// Offset at which to cut off the rest. This will be the size after the operation. + offset: u32, + }, + + /// Verify the data of a program account to be a valid ELF. + /// + /// If this succeeds the program becomes executable, and is ready to use. + /// A source program account can be provided to overwrite the data before deployment + /// in one step, instead retracting the program and writing to it and redeploying it. + /// The source program is truncated to zero (thus closed) and lamports necessary for + /// rent exemption are transferred, in case that the source was bigger than the program. + /// + /// # Account references + /// 0. `[writable]` The program account to deploy. + /// 1. `[signer]` The authority of the program. + /// 2. `[writable]` Optional, an undeployed source program account to take data and lamports from. + Deploy, + + /// Undo the deployment of a program account. + /// + /// The program is no longer executable and goes into maintainance. + /// Necessary for writing data and truncating. + /// + /// # Account references + /// 0. `[writable]` The program account to retract. + /// 1. `[signer]` The authority of the program. + Retract, + + /// Transfers the authority over a program account. + /// + /// WARNING: Using this instruction without providing a new authority + /// finalizes the program (it becomes immutable). + /// + /// # Account references + /// 0. `[writable]` The program account to change the authority of. + /// 1. `[signer]` The current authority of the program. + /// 2. `[signer]` The new authority of the program. Optional if program is currently deployed. + TransferAuthority, +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 8459bab298..a18ac583a8 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -47,11 +47,12 @@ pub use solana_program::{ bpf_loader_deprecated, bpf_loader_upgradeable, clock, config, custom_heap_default, custom_panic_default, debug_account_data, declare_deprecated_sysvar_id, declare_sysvar_id, decode_error, ed25519_program, epoch_schedule, fee_calculator, impl_sysvar_get, incinerator, - instruction, keccak, lamports, loader_instruction, loader_upgradeable_instruction, message, - msg, native_token, nonce, program, program_error, program_memory, program_option, program_pack, - rent, sanitize, sdk_ids, secp256k1_program, secp256k1_recover, serde_varint, serialize_utils, - short_vec, slot_hashes, slot_history, stable_layout, stake, stake_history, syscalls, - system_instruction, system_program, sysvar, unchecked_div_by_const, vote, wasm_bindgen, + instruction, keccak, lamports, loader_instruction, loader_upgradeable_instruction, loader_v3, + loader_v3_instruction, message, msg, native_token, nonce, program, program_error, + program_memory, program_option, program_pack, rent, sanitize, sdk_ids, secp256k1_program, + secp256k1_recover, serde_varint, serialize_utils, short_vec, slot_hashes, slot_history, + stable_layout, stake, stake_history, syscalls, system_instruction, system_program, sysvar, + unchecked_div_by_const, vote, wasm_bindgen, }; pub mod account;