diff --git a/Cargo.lock b/Cargo.lock index 2fb61646b042b1..019d37d52f9f02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5780,6 +5780,7 @@ dependencies = [ "solana-program-runtime", "solana-pubsub-client", "solana-remote-wallet", + "solana-rpc", "solana-rpc-client", "solana-rpc-client-api", "solana-rpc-client-nonce-utils", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b9170ac79ab07c..b5e444ba81a00b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -56,6 +56,7 @@ tiny-bip39 = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } +solana-rpc = { workspace = true } solana-streamer = { workspace = true } solana-test-validator = { workspace = true } tempfile = { workspace = true } diff --git a/cli/src/program.rs b/cli/src/program.rs index c35871868f0f04..0aec785fa445ea 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -13,6 +13,7 @@ use { solana_bpf_loader_program::syscalls::create_program_runtime_environment_v1, solana_clap_utils::{ self, + compute_unit_price::compute_unit_price_arg, fee_payer::{fee_payer_arg, FEE_PAYER_ARG}, hidden_unless_forced, input_parsers::*, @@ -28,12 +29,16 @@ use { }, solana_client::{ connection_cache::ConnectionCache, + rpc_config::RpcSimulateTransactionConfig, send_and_confirm_transactions_in_parallel::{ send_and_confirm_transactions_in_parallel_blocking, SendAndConfirmConfig, }, tpu_client::{TpuClient, TpuClientConfig}, }, - solana_program_runtime::{compute_budget::ComputeBudget, invoke_context::InvokeContext}, + solana_program_runtime::{ + compute_budget::ComputeBudget, compute_budget_processor::MAX_COMPUTE_UNIT_LIMIT, + invoke_context::InvokeContext, + }, solana_rbpf::{elf::Executable, verifier::RequisiteVerifier}, solana_remote_wallet::remote_wallet::RemoteWalletManager, solana_rpc_client::rpc_client::RpcClient, @@ -46,8 +51,10 @@ use { solana_sdk::{ account::Account, account_utils::StateMut, + borsh1::try_from_slice_unchecked, bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable::{self, UpgradeableLoaderState}, + compute_budget::{self, ComputeBudgetInstruction}, feature_set::FeatureSet, instruction::{Instruction, InstructionError}, loader_instruction, @@ -90,6 +97,7 @@ pub enum ProgramCliCommand { max_len: Option, allow_excessive_balance: bool, skip_fee_check: bool, + compute_unit_price: Option, }, Upgrade { fee_payer_signer_index: SignerIndex, @@ -108,6 +116,7 @@ pub enum ProgramCliCommand { buffer_authority_signer_index: SignerIndex, max_len: Option, skip_fee_check: bool, + compute_unit_price: Option, }, SetBufferAuthority { buffer_pubkey: Pubkey, @@ -236,7 +245,8 @@ impl ProgramSubCommands for App<'_, '_> { "Use the designated program id even if the account already \ holds a large balance of SOL", ), - ), + ) + .arg(compute_unit_price_arg()), ) .subcommand( SubCommand::with_name("upgrade") @@ -308,7 +318,8 @@ impl ProgramSubCommands for App<'_, '_> { "Maximum length of the upgradeable program \ [default: the length of the original deployed program]", ), - ), + ) + .arg(compute_unit_price_arg()), ) .subcommand( SubCommand::with_name("set-buffer-authority") @@ -601,6 +612,8 @@ pub fn parse_program_subcommand( let signer_info = default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; + let compute_unit_price = value_of(matches, "compute_unit_price"); + CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Deploy { program_location, @@ -616,6 +629,7 @@ pub fn parse_program_subcommand( max_len, allow_excessive_balance: matches.is_present("allow_excessive_balance"), skip_fee_check, + compute_unit_price, }), signers: signer_info.signers, } @@ -687,6 +701,8 @@ pub fn parse_program_subcommand( let signer_info = default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; + let compute_unit_price = value_of(matches, "compute_unit_price"); + CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: matches.value_of("program_location").unwrap().to_string(), @@ -698,6 +714,7 @@ pub fn parse_program_subcommand( .unwrap(), max_len, skip_fee_check, + compute_unit_price, }), signers: signer_info.signers, } @@ -899,6 +916,7 @@ pub fn process_program_subcommand( max_len, allow_excessive_balance, skip_fee_check, + compute_unit_price, } => process_program_deploy( rpc_client, config, @@ -913,6 +931,7 @@ pub fn process_program_subcommand( *max_len, *allow_excessive_balance, *skip_fee_check, + *compute_unit_price, ), ProgramCliCommand::Upgrade { fee_payer_signer_index, @@ -941,6 +960,7 @@ pub fn process_program_subcommand( buffer_authority_signer_index, max_len, skip_fee_check, + compute_unit_price, } => process_write_buffer( rpc_client, config, @@ -951,6 +971,7 @@ pub fn process_program_subcommand( *buffer_authority_signer_index, *max_len, *skip_fee_check, + *compute_unit_price, ), ProgramCliCommand::SetBufferAuthority { buffer_pubkey, @@ -1082,6 +1103,7 @@ fn process_program_deploy( max_len: Option, allow_excessive_balance: bool, skip_fee_check: bool, + compute_unit_price: Option, ) -> ProcessResult { let fee_payer_signer = config.signers[fee_payer_signer_index]; let upgrade_authority_signer = config.signers[upgrade_authority_signer_index]; @@ -1221,6 +1243,7 @@ fn process_program_deploy( upgrade_authority_signer, allow_excessive_balance, skip_fee_check, + compute_unit_price, ) } else { do_process_program_upgrade( @@ -1235,6 +1258,7 @@ fn process_program_deploy( &buffer_pubkey, buffer_signer, skip_fee_check, + compute_unit_price, ) }; if result.is_ok() && is_final { @@ -1372,6 +1396,7 @@ fn process_program_upgrade( } } +#[allow(clippy::too_many_arguments)] fn process_write_buffer( rpc_client: Arc, config: &CliConfig, @@ -1382,6 +1407,7 @@ fn process_write_buffer( buffer_authority_signer_index: SignerIndex, max_len: Option, skip_fee_check: bool, + compute_unit_price: Option, ) -> ProcessResult { let fee_payer_signer = config.signers[fee_payer_signer_index]; let buffer_authority = config.signers[buffer_authority_signer_index]; @@ -1447,6 +1473,7 @@ fn process_write_buffer( buffer_authority, true, skip_fee_check, + compute_unit_price, ); if result.is_err() && buffer_signer_index.is_none() && buffer_signer.is_some() { report_ephemeral_mnemonic(words, mnemonic); @@ -2200,11 +2227,12 @@ fn do_process_program_write_and_deploy( buffer_authority_signer: &dyn Signer, allow_excessive_balance: bool, skip_fee_check: bool, + compute_unit_price: Option, ) -> ProcessResult { let blockhash = rpc_client.get_latest_blockhash()?; // Initialize buffer account or complete if already partially initialized - let (initial_instructions, balance_needed, buffer_program_data) = if let Some(mut account) = + let (mut initial_instructions, balance_needed, buffer_program_data) = if let Some(mut account) = rpc_client .get_account_with_commitment(buffer_pubkey, config.commitment)? .value @@ -2251,7 +2279,9 @@ fn do_process_program_write_and_deploy( vec![0; program_len], ) }; + let initial_message = if !initial_instructions.is_empty() { + set_compute_budget_ixs_if_needed(&mut initial_instructions, compute_unit_price); Some(Message::new_with_blockhash( &initial_instructions, Some(&fee_payer_signer.pubkey()), @@ -2273,7 +2303,10 @@ fn do_process_program_write_and_deploy( } else { loader_instruction::write(buffer_pubkey, loader_id, offset, bytes) }; - Message::new_with_blockhash(&[instruction], Some(&fee_payer_signer.pubkey()), &blockhash) + + let mut instructions = vec![instruction]; + set_compute_budget_ixs_if_needed(&mut instructions, compute_unit_price); + Message::new_with_blockhash(&instructions, Some(&fee_payer_signer.pubkey()), &blockhash) }; let mut write_messages = vec![]; @@ -2288,26 +2321,23 @@ fn do_process_program_write_and_deploy( // Create and add final message let final_message = if let Some(program_signers) = program_signers { let message = if loader_id == &bpf_loader_upgradeable::id() { - Message::new_with_blockhash( - &bpf_loader_upgradeable::deploy_with_max_program_len( - &fee_payer_signer.pubkey(), - &program_signers[0].pubkey(), - buffer_pubkey, - &program_signers[1].pubkey(), - rpc_client.get_minimum_balance_for_rent_exemption( - UpgradeableLoaderState::size_of_program(), - )?, - program_data_max_len, + let mut instructions = bpf_loader_upgradeable::deploy_with_max_program_len( + &fee_payer_signer.pubkey(), + &program_signers[0].pubkey(), + buffer_pubkey, + &program_signers[1].pubkey(), + rpc_client.get_minimum_balance_for_rent_exemption( + UpgradeableLoaderState::size_of_program(), )?, - Some(&fee_payer_signer.pubkey()), - &blockhash, - ) + program_data_max_len, + )?; + + set_compute_budget_ixs_if_needed(&mut instructions, compute_unit_price); + Message::new_with_blockhash(&instructions, Some(&fee_payer_signer.pubkey()), &blockhash) } else { - Message::new_with_blockhash( - &[loader_instruction::finalize(buffer_pubkey, loader_id)], - Some(&fee_payer_signer.pubkey()), - &blockhash, - ) + let mut instructions = vec![loader_instruction::finalize(buffer_pubkey, loader_id)]; + set_compute_budget_ixs_if_needed(&mut instructions, compute_unit_price); + Message::new_with_blockhash(&instructions, Some(&fee_payer_signer.pubkey()), &blockhash) }; Some(message) } else { @@ -2365,93 +2395,97 @@ fn do_process_program_upgrade( buffer_pubkey: &Pubkey, buffer_signer: Option<&dyn Signer>, skip_fee_check: bool, + compute_unit_price: Option, ) -> ProcessResult { let blockhash = rpc_client.get_latest_blockhash()?; - let (initial_message, write_messages, balance_needed) = - if let Some(buffer_signer) = buffer_signer { - // Check Buffer account to see if partial initialization has occurred - let (initial_instructions, balance_needed, buffer_program_data) = - if let Some(mut account) = rpc_client - .get_account_with_commitment(&buffer_signer.pubkey(), config.commitment)? - .value - { - let (ixs, balance_needed) = complete_partial_program_init( - &bpf_loader_upgradeable::id(), + let (initial_message, write_messages, balance_needed) = if let Some(buffer_signer) = + buffer_signer + { + // Check Buffer account to see if partial initialization has occurred + let (mut initial_instructions, balance_needed, buffer_program_data) = + if let Some(mut account) = rpc_client + .get_account_with_commitment(&buffer_signer.pubkey(), config.commitment)? + .value + { + let (ixs, balance_needed) = complete_partial_program_init( + &bpf_loader_upgradeable::id(), + &fee_payer_signer.pubkey(), + &buffer_signer.pubkey(), + &account, + UpgradeableLoaderState::size_of_buffer(program_len), + min_rent_exempt_program_data_balance, + true, + )?; + let buffer_program_data = account + .data + .split_off(UpgradeableLoaderState::size_of_buffer_metadata()); + (ixs, balance_needed, buffer_program_data) + } else { + ( + bpf_loader_upgradeable::create_buffer( &fee_payer_signer.pubkey(), - &buffer_signer.pubkey(), - &account, - UpgradeableLoaderState::size_of_buffer(program_len), - min_rent_exempt_program_data_balance, - true, - )?; - let buffer_program_data = account - .data - .split_off(UpgradeableLoaderState::size_of_buffer_metadata()); - (ixs, balance_needed, buffer_program_data) - } else { - ( - bpf_loader_upgradeable::create_buffer( - &fee_payer_signer.pubkey(), - buffer_pubkey, - &upgrade_authority.pubkey(), - min_rent_exempt_program_data_balance, - program_len, - )?, + buffer_pubkey, + &upgrade_authority.pubkey(), min_rent_exempt_program_data_balance, - vec![0; program_len], - ) - }; - - let initial_message = if !initial_instructions.is_empty() { - Some(Message::new_with_blockhash( - &initial_instructions, - Some(&fee_payer_signer.pubkey()), - &blockhash, - )) - } else { - None - }; - - let buffer_signer_pubkey = buffer_signer.pubkey(); - let upgrade_authority_pubkey = upgrade_authority.pubkey(); - let create_msg = |offset: u32, bytes: Vec| { - let instruction = bpf_loader_upgradeable::write( - &buffer_signer_pubkey, - &upgrade_authority_pubkey, - offset, - bytes, - ); - Message::new_with_blockhash( - &[instruction], - Some(&fee_payer_signer.pubkey()), - &blockhash, + program_len, + )?, + min_rent_exempt_program_data_balance, + vec![0; program_len], ) }; - // Create and add write messages - let mut write_messages = vec![]; - let chunk_size = calculate_max_chunk_size(&create_msg); - for (chunk, i) in program_data.chunks(chunk_size).zip(0..) { - let offset = i * chunk_size; - if chunk != &buffer_program_data[offset..offset + chunk.len()] { - write_messages.push(create_msg(offset as u32, chunk.to_vec())); - } - } - - (initial_message, write_messages, balance_needed) + let initial_message = if !initial_instructions.is_empty() { + set_compute_budget_ixs_if_needed(&mut initial_instructions, compute_unit_price); + Some(Message::new_with_blockhash( + &initial_instructions, + Some(&fee_payer_signer.pubkey()), + &blockhash, + )) } else { - (None, vec![], 0) + None + }; + + let buffer_signer_pubkey = buffer_signer.pubkey(); + let upgrade_authority_pubkey = upgrade_authority.pubkey(); + let create_msg = |offset: u32, bytes: Vec| { + let mut instructions = vec![bpf_loader_upgradeable::write( + &buffer_signer_pubkey, + &upgrade_authority_pubkey, + offset, + bytes, + )]; + + set_compute_budget_ixs_if_needed(&mut instructions, compute_unit_price); + Message::new_with_blockhash(&instructions, Some(&fee_payer_signer.pubkey()), &blockhash) }; + // Create and add write messages + let mut write_messages = vec![]; + let chunk_size = calculate_max_chunk_size(&create_msg); + for (chunk, i) in program_data.chunks(chunk_size).zip(0..) { + let offset = i * chunk_size; + if chunk != &buffer_program_data[offset..offset + chunk.len()] { + write_messages.push(create_msg(offset as u32, chunk.to_vec())); + } + } + + (initial_message, write_messages, balance_needed) + } else { + (None, vec![], 0) + }; + // Create and add final message + let mut final_instructions = vec![bpf_loader_upgradeable::upgrade( + program_id, + buffer_pubkey, + &upgrade_authority.pubkey(), + &fee_payer_signer.pubkey(), + )]; + + set_compute_budget_ixs_if_needed(&mut final_instructions, compute_unit_price); let final_message = Message::new_with_blockhash( - &[bpf_loader_upgradeable::upgrade( - program_id, - buffer_pubkey, - &upgrade_authority.pubkey(), - &fee_payer_signer.pubkey(), - )], + &final_instructions, Some(&fee_payer_signer.pubkey()), &blockhash, ); @@ -2598,6 +2632,70 @@ fn check_payer( Ok(()) } +// This enum is equivalent to an Option but was added to self-document +// the ok variants and has the benefit of not forcing the caller to use +// the result if they don't care about it. +enum UpdateComputeUnitLimitResult { + UpdatedInstructionIndex(usize), + NoInstructionFound, +} + +// Returns the index of the compute unit limit instruction +fn simulate_and_update_compute_unit_limit( + rpc_client: &RpcClient, + transaction: &mut Transaction, +) -> Result> { + let Some(compute_unit_limit_ix_index) = transaction + .message + .instructions + .iter() + .enumerate() + .find_map(|(ix_index, instruction)| { + let ix_program_id = transaction.message.program_id(ix_index)?; + if ix_program_id != &compute_budget::id() { + return None; + } + + matches!( + try_from_slice_unchecked(&instruction.data), + Ok(ComputeBudgetInstruction::SetComputeUnitLimit(_)) + ) + .then_some(ix_index) + }) + else { + return Ok(UpdateComputeUnitLimitResult::NoInstructionFound); + }; + + let simulate_result = rpc_client + .simulate_transaction_with_config( + transaction, + RpcSimulateTransactionConfig { + replace_recent_blockhash: true, + commitment: Some(rpc_client.commitment()), + ..RpcSimulateTransactionConfig::default() + }, + )? + .value; + + // Bail if the simulated transaction failed + if let Some(err) = simulate_result.err { + return Err(err.into()); + } + + let units_consumed = simulate_result + .units_consumed + .expect("compute units unavailable"); + + // Overwrite the compute unit limit instruction with the actual units consumed + let compute_unit_limit = u32::try_from(units_consumed)?; + transaction.message.instructions[compute_unit_limit_ix_index].data = + ComputeBudgetInstruction::set_compute_unit_limit(compute_unit_limit).data; + + Ok(UpdateComputeUnitLimitResult::UpdatedInstructionIndex( + compute_unit_limit_ix_index, + )) +} + fn send_deploy_messages( rpc_client: Arc, config: &CliConfig, @@ -2612,9 +2710,12 @@ fn send_deploy_messages( if let Some(message) = initial_message { if let Some(initial_signer) = initial_signer { trace!("Preparing the required accounts"); - let blockhash = rpc_client.get_latest_blockhash()?; let mut initial_transaction = Transaction::new_unsigned(message.clone()); + simulate_and_update_compute_unit_limit(&rpc_client, &mut initial_transaction)?; + + let blockhash = rpc_client.get_latest_blockhash()?; + // Most of the initial_transaction combinations require both the fee-payer and new program // account to sign the transaction. One (transfer) only requires the fee-payer signature. // This check is to ensure signing does not fail on a KeypairPubkeyMismatch error from an @@ -2635,6 +2736,29 @@ fn send_deploy_messages( if !write_messages.is_empty() { if let Some(write_signer) = write_signer { trace!("Writing program data"); + + // Simulate the first write message to get the number of compute units + // consumed and then reuse that value as the compute unit limit for all + // write messages. + let mut write_messages = write_messages.to_vec(); + { + let mut transaction = Transaction::new_unsigned(write_messages[0].clone()); + if let UpdateComputeUnitLimitResult::UpdatedInstructionIndex(ix_index) = + simulate_and_update_compute_unit_limit(&rpc_client, &mut transaction)? + { + for msg in &mut write_messages { + // Write messages are all assumed to be identical except + // the program data being written. But just in case that + // assumption is broken, assert that we are only ever + // changing the instruction data for a compute budget + // instruction. + assert_eq!(msg.program_id(ix_index), Some(&compute_budget::id())); + msg.instructions[ix_index].data = + transaction.message.instructions[ix_index].data.clone(); + } + } + } + let connection_cache = if config.use_quic { ConnectionCache::new_quic("connection_cache_cli_program_quic", 1) } else { @@ -2648,7 +2772,7 @@ fn send_deploy_messages( cache, )? .send_and_confirm_messages_with_spinner( - write_messages, + &write_messages, &[fee_payer_signer, write_signer], ), ConnectionCache::Quic(cache) => { @@ -2666,7 +2790,7 @@ fn send_deploy_messages( send_and_confirm_transactions_in_parallel_blocking( rpc_client.clone(), Some(tpu_client), - write_messages, + &write_messages, &[fee_payer_signer, write_signer], SendAndConfirmConfig { resign_txs_count: Some(5), @@ -2694,9 +2818,11 @@ fn send_deploy_messages( if let Some(message) = final_message { if let Some(final_signers) = final_signers { trace!("Deploying program"); - let blockhash = rpc_client.get_latest_blockhash()?; let mut final_tx = Transaction::new_unsigned(message.clone()); + simulate_and_update_compute_unit_limit(&rpc_client, &mut final_tx)?; + + let blockhash = rpc_client.get_latest_blockhash()?; let mut signers = final_signers.to_vec(); signers.push(fee_payer_signer); final_tx.try_sign(&signers, blockhash)?; @@ -2740,6 +2866,24 @@ fn report_ephemeral_mnemonic(words: usize, mnemonic: bip39::Mnemonic) { eprintln!("[BUFFER_ACCOUNT_ADDRESS] argument to `solana program close`.\n{divider}"); } +fn set_compute_budget_ixs_if_needed(ixs: &mut Vec, compute_unit_price: Option) { + let Some(compute_unit_price) = compute_unit_price else { + return; + }; + + // Default to the max compute unit limit because later transactions will be + // simulated to get the exact compute units consumed. + ixs.insert( + 0, + ComputeBudgetInstruction::set_compute_unit_limit(MAX_COMPUTE_UNIT_LIMIT), + ); + + ixs.insert( + 0, + ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price), + ); +} + #[cfg(test)] mod tests { use { @@ -2798,6 +2942,7 @@ mod tests { max_len: None, allow_excessive_balance: false, skip_fee_check: false, + compute_unit_price: None }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], } @@ -2826,6 +2971,7 @@ mod tests { max_len: Some(42), allow_excessive_balance: false, skip_fee_check: false, + compute_unit_price: None }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], } @@ -2856,6 +3002,7 @@ mod tests { max_len: None, allow_excessive_balance: false, skip_fee_check: false, + compute_unit_price: None }), signers: vec![ Box::new(read_keypair_file(&keypair_file).unwrap()), @@ -2888,6 +3035,7 @@ mod tests { max_len: None, allow_excessive_balance: false, skip_fee_check: false, + compute_unit_price: None }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], } @@ -2919,6 +3067,7 @@ mod tests { max_len: None, allow_excessive_balance: false, skip_fee_check: false, + compute_unit_price: None }), signers: vec![ Box::new(read_keypair_file(&keypair_file).unwrap()), @@ -2953,6 +3102,7 @@ mod tests { max_len: None, allow_excessive_balance: false, skip_fee_check: false, + compute_unit_price: None }), signers: vec![ Box::new(read_keypair_file(&keypair_file).unwrap()), @@ -2983,6 +3133,7 @@ mod tests { max_len: None, skip_fee_check: false, allow_excessive_balance: false, + compute_unit_price: None }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], } @@ -3017,6 +3168,7 @@ mod tests { buffer_authority_signer_index: 0, max_len: None, skip_fee_check: false, + compute_unit_price: None }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], } @@ -3042,6 +3194,7 @@ mod tests { buffer_authority_signer_index: 0, max_len: Some(42), skip_fee_check: false, + compute_unit_price: None }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], } @@ -3070,6 +3223,7 @@ mod tests { buffer_authority_signer_index: 0, max_len: None, skip_fee_check: false, + compute_unit_price: None }), signers: vec![ Box::new(read_keypair_file(&keypair_file).unwrap()), @@ -3101,6 +3255,7 @@ mod tests { buffer_authority_signer_index: 1, max_len: None, skip_fee_check: false, + compute_unit_price: None }), signers: vec![ Box::new(read_keypair_file(&keypair_file).unwrap()), @@ -3137,6 +3292,7 @@ mod tests { buffer_authority_signer_index: 2, max_len: None, skip_fee_check: false, + compute_unit_price: None }), signers: vec![ Box::new(read_keypair_file(&keypair_file).unwrap()), @@ -3695,6 +3851,7 @@ mod tests { max_len: None, allow_excessive_balance: false, skip_fee_check: false, + compute_unit_price: None, }), signers: vec![&default_keypair], output_format: OutputFormat::JsonCompact, diff --git a/cli/tests/program.rs b/cli/tests/program.rs index 6eb281d65b9e35..240a01567a409e 100644 --- a/cli/tests/program.rs +++ b/cli/tests/program.rs @@ -3,6 +3,7 @@ #![allow(clippy::items_after_test_module)] use { + assert_matches::assert_matches, serde_json::Value, solana_cli::{ cli::{process_command, CliCommand, CliConfig}, @@ -10,18 +11,29 @@ use { test_utils::wait_n_slots, }, solana_cli_output::{parse_sign_only_reply_string, OutputFormat}, + solana_client::{ + rpc_client::GetConfirmedSignaturesForAddress2Config, rpc_config::RpcTransactionConfig, + }, solana_faucet::faucet::run_local_faucet, + solana_rpc::rpc::JsonRpcConfig, solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_nonce_utils::blockhash_query::BlockhashQuery, solana_sdk::{ account_utils::StateMut, + borsh1::try_from_slice_unchecked, bpf_loader_upgradeable::{self, UpgradeableLoaderState}, commitment_config::CommitmentConfig, + compute_budget::{self, ComputeBudgetInstruction}, + fee_calculator::FeeRateGovernor, pubkey::Pubkey, - signature::{Keypair, NullSigner, Signer}, + rent::Rent, + signature::{Keypair, NullSigner, Signature, Signer}, + system_program, + transaction::Transaction, }, solana_streamer::socket::SocketAddrSpace, - solana_test_validator::TestValidator, + solana_test_validator::{TestValidator, TestValidatorGenesis}, + solana_transaction_status::UiTransactionEncoding, std::{ env, fs::File, @@ -85,6 +97,7 @@ fn test_cli_program_deploy_non_upgradeable() { is_final: true, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; let response = process_command(&config); @@ -131,6 +144,7 @@ fn test_cli_program_deploy_non_upgradeable() { is_final: true, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap(); let account1 = rpc_client @@ -186,6 +200,7 @@ fn test_cli_program_deploy_non_upgradeable() { is_final: true, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); let err = process_command(&config).unwrap_err(); assert_eq!( @@ -209,6 +224,7 @@ fn test_cli_program_deploy_non_upgradeable() { is_final: true, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap_err(); } @@ -270,6 +286,7 @@ fn test_cli_program_deploy_no_authority() { is_final: true, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; let response = process_command(&config); @@ -297,6 +314,7 @@ fn test_cli_program_deploy_no_authority() { is_final: false, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap_err(); } @@ -359,6 +377,7 @@ fn test_cli_program_deploy_with_authority() { is_final: false, max_len: Some(max_len), skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; let response = process_command(&config); @@ -408,6 +427,7 @@ fn test_cli_program_deploy_with_authority() { is_final: false, max_len: Some(max_len), skip_fee_check: false, + compute_unit_price: None, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -451,6 +471,7 @@ fn test_cli_program_deploy_with_authority() { is_final: false, max_len: Some(max_len), skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap(); let program_account = rpc_client.get_account(&program_pubkey).unwrap(); @@ -526,6 +547,7 @@ fn test_cli_program_deploy_with_authority() { is_final: false, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap(); let program_account = rpc_client.get_account(&program_pubkey).unwrap(); @@ -605,6 +627,7 @@ fn test_cli_program_deploy_with_authority() { is_final: false, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap_err(); @@ -622,6 +645,7 @@ fn test_cli_program_deploy_with_authority() { is_final: true, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -726,6 +750,7 @@ fn test_cli_program_close_program() { is_final: false, max_len: Some(max_len), skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; process_command(&config).unwrap(); @@ -836,6 +861,7 @@ fn test_cli_program_extend_program() { is_final: false, max_len: None, // Use None to check that it defaults to the max length skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; process_command(&config).unwrap(); @@ -883,6 +909,7 @@ fn test_cli_program_extend_program() { is_final: false, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap_err(); @@ -915,6 +942,7 @@ fn test_cli_program_extend_program() { is_final: false, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap(); } @@ -979,6 +1007,7 @@ fn test_cli_program_write_buffer() { buffer_authority_signer_index: 0, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; let response = process_command(&config); @@ -1015,6 +1044,7 @@ fn test_cli_program_write_buffer() { buffer_authority_signer_index: 0, max_len: Some(max_len), skip_fee_check: false, + compute_unit_price: None, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -1078,6 +1108,7 @@ fn test_cli_program_write_buffer() { buffer_authority_signer_index: 2, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -1117,6 +1148,7 @@ fn test_cli_program_write_buffer() { buffer_authority_signer_index: 2, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -1192,6 +1224,7 @@ fn test_cli_program_write_buffer() { buffer_authority_signer_index: 0, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; let response = process_command(&config); @@ -1234,6 +1267,7 @@ fn test_cli_program_write_buffer() { buffer_authority_signer_index: 0, max_len: None, //Some(max_len), skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap(); config.signers = vec![&keypair, &buffer_keypair]; @@ -1249,6 +1283,7 @@ fn test_cli_program_write_buffer() { is_final: true, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; let error = process_command(&config).unwrap_err(); @@ -1308,6 +1343,7 @@ fn test_cli_program_set_buffer_authority() { buffer_authority_signer_index: 0, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap(); let buffer_account = rpc_client.get_account(&buffer_keypair.pubkey()).unwrap(); @@ -1360,6 +1396,7 @@ fn test_cli_program_set_buffer_authority() { is_final: false, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; process_command(&config).unwrap_err(); @@ -1405,6 +1442,7 @@ fn test_cli_program_set_buffer_authority() { is_final: false, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; process_command(&config).unwrap(); @@ -1461,6 +1499,7 @@ fn test_cli_program_mismatch_buffer_authority() { buffer_authority_signer_index: 2, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap(); let buffer_account = rpc_client.get_account(&buffer_keypair.pubkey()).unwrap(); @@ -1485,6 +1524,7 @@ fn test_cli_program_mismatch_buffer_authority() { is_final: true, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap_err(); @@ -1502,6 +1542,7 @@ fn test_cli_program_mismatch_buffer_authority() { is_final: true, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap(); } @@ -1585,6 +1626,7 @@ fn test_cli_program_deploy_with_offline_signing(use_offline_signer_as_fee_payer: is_final: false, max_len: Some(max_program_data_len), // allows for larger program size with future upgrades skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; process_command(&config).unwrap(); @@ -1752,6 +1794,7 @@ fn test_cli_program_show() { buffer_authority_signer_index: 2, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap(); @@ -1813,6 +1856,7 @@ fn test_cli_program_show() { is_final: false, max_len: Some(max_len), skip_fee_check: false, + compute_unit_price: None, }); config.output_format = OutputFormat::JsonCompact; let min_slot = rpc_client.get_slot().unwrap(); @@ -1941,6 +1985,7 @@ fn test_cli_program_dump() { buffer_authority_signer_index: 2, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(&config).unwrap(); @@ -1984,6 +2029,7 @@ fn create_buffer_with_offline_authority<'a>( buffer_authority_signer_index: 0, max_len: None, skip_fee_check: false, + compute_unit_price: None, }); process_command(config).unwrap(); let buffer_account = rpc_client.get_account(&buffer_signer.pubkey()).unwrap(); @@ -2009,3 +2055,179 @@ fn create_buffer_with_offline_authority<'a>( panic!("not a buffer account"); } } + +#[allow(clippy::assertions_on_constants)] +fn cli_program_deploy_with_args(compute_unit_price: Option) { + let mut noop_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + noop_path.push("tests"); + noop_path.push("fixtures"); + noop_path.push("noop"); + noop_path.set_extension("so"); + + let mint_keypair = Keypair::new(); + let mint_pubkey = mint_keypair.pubkey(); + let faucet_addr = run_local_faucet(mint_keypair, None); + let test_validator = TestValidatorGenesis::default() + .fee_rate_governor(FeeRateGovernor::new(0, 0)) + .rent(Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }) + .rpc_config(JsonRpcConfig { + enable_rpc_transaction_history: true, + faucet_addr: Some(faucet_addr), + ..JsonRpcConfig::default_for_test() + }) + .start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified) + .expect("validator start failed"); + + let rpc_client = + RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::confirmed()); + + let mut file = File::open(noop_path.to_str().unwrap()).unwrap(); + let mut program_data = Vec::new(); + file.read_to_end(&mut program_data).unwrap(); + let max_len = program_data.len(); + let minimum_balance_for_programdata = rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_programdata( + max_len, + )) + .unwrap(); + let minimum_balance_for_program = rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()) + .unwrap(); + let upgrade_authority = Keypair::new(); + + let mut config = CliConfig::recent_for_tests(); + let keypair = Keypair::new(); + config.json_rpc_url = test_validator.rpc_url(); + config.signers = vec![&keypair]; + config.command = CliCommand::Airdrop { + pubkey: None, + lamports: 100 * minimum_balance_for_programdata + minimum_balance_for_program, + }; + process_command(&config).unwrap(); + + // Deploy the upgradeable program with specified program_id + let program_keypair = Keypair::new(); + config.signers = vec![&keypair, &upgrade_authority, &program_keypair]; + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: Some(noop_path.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, + program_signer_index: Some(2), + program_pubkey: Some(program_keypair.pubkey()), + buffer_signer_index: None, + buffer_pubkey: None, + allow_excessive_balance: false, + upgrade_authority_signer_index: 1, + is_final: false, + max_len: Some(max_len), + skip_fee_check: false, + compute_unit_price, + }); + config.output_format = OutputFormat::JsonCompact; + let response = process_command(&config); + let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); + let program_pubkey_str = json + .as_object() + .unwrap() + .get("programId") + .unwrap() + .as_str() + .unwrap(); + assert_eq!( + program_keypair.pubkey(), + Pubkey::from_str(program_pubkey_str).unwrap() + ); + let program_account = rpc_client.get_account(&program_keypair.pubkey()).unwrap(); + assert_eq!(program_account.lamports, minimum_balance_for_program); + assert_eq!(program_account.owner, bpf_loader_upgradeable::id()); + assert!(program_account.executable); + let signature_statuses = rpc_client + .get_signatures_for_address_with_config( + &keypair.pubkey(), + GetConfirmedSignaturesForAddress2Config { + commitment: Some(CommitmentConfig::confirmed()), + ..GetConfirmedSignaturesForAddress2Config::default() + }, + ) + .unwrap(); + let signatures: Vec<_> = signature_statuses + .into_iter() + .rev() + .map(|status| Signature::from_str(&status.signature).unwrap()) + .collect(); + + fn fetch_and_decode_transaction(rpc_client: &RpcClient, signature: &Signature) -> Transaction { + rpc_client + .get_transaction_with_config( + signature, + RpcTransactionConfig { + encoding: Some(UiTransactionEncoding::Base64), + commitment: Some(CommitmentConfig::confirmed()), + ..RpcTransactionConfig::default() + }, + ) + .unwrap() + .transaction + .transaction + .decode() + .unwrap() + .into_legacy_transaction() + .unwrap() + } + + assert!(signatures.len() >= 4); + let initial_tx = fetch_and_decode_transaction(&rpc_client, &signatures[1]); + let write_tx = fetch_and_decode_transaction(&rpc_client, &signatures[2]); + let final_tx = fetch_and_decode_transaction(&rpc_client, signatures.last().unwrap()); + + if let Some(compute_unit_price) = compute_unit_price { + for tx in [&initial_tx, &write_tx, &final_tx] { + for i in [0, 1] { + assert_eq!( + tx.message.instructions[i].program_id(&tx.message.account_keys), + &compute_budget::id() + ); + } + + assert_matches!( + try_from_slice_unchecked(&tx.message.instructions[0].data), + Ok(ComputeBudgetInstruction::SetComputeUnitPrice(price)) if price == compute_unit_price + ); + } + + assert_matches!( + try_from_slice_unchecked(&initial_tx.message.instructions[1].data), + Ok(ComputeBudgetInstruction::SetComputeUnitLimit(2820)) + ); + assert_matches!( + try_from_slice_unchecked(&write_tx.message.instructions[1].data), + Ok(ComputeBudgetInstruction::SetComputeUnitLimit(2670)) + ); + assert_matches!( + try_from_slice_unchecked(&final_tx.message.instructions[1].data), + Ok(ComputeBudgetInstruction::SetComputeUnitLimit(2970)) + ); + } else { + assert_eq!( + initial_tx.message.instructions[0].program_id(&initial_tx.message.account_keys), + &system_program::id() + ); + assert_eq!( + write_tx.message.instructions[0].program_id(&write_tx.message.account_keys), + &bpf_loader_upgradeable::id() + ); + assert_eq!( + final_tx.message.instructions[0].program_id(&final_tx.message.account_keys), + &system_program::id() + ); + } +} + +#[test] +fn test_cli_program_deploy_with_compute_unit_price() { + cli_program_deploy_with_args(Some(1000)); + cli_program_deploy_with_args(None); +} diff --git a/transaction-dos/src/main.rs b/transaction-dos/src/main.rs index dedbcdab27ef79..3cf835c578382e 100644 --- a/transaction-dos/src/main.rs +++ b/transaction-dos/src/main.rs @@ -247,6 +247,7 @@ fn run_transactions_dos( upgrade_authority_signer_index: 0, is_final: true, max_len: None, + compute_unit_price: None, skip_fee_check: true, // skip_fee_check });