diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index 7fdf14b24e..eadc789e36 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -58,6 +58,15 @@ impl CliSignerInfo { Some(0) } } + pub fn index_of_or_none(&self, pubkey: Option) -> Option { + if let Some(pubkey) = pubkey { + self.signers + .iter() + .position(|signer| signer.pubkey() == pubkey) + } else { + None + } + } } pub struct DefaultSigner { diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 237e02c1ff..5ac4342aec 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,15 +1,12 @@ use crate::{ - checks::*, cluster_query::*, feature::*, inflation::*, nonce::*, send_tpu::*, spend_utils::*, - stake::*, validator_info::*, vote::*, + cluster_query::*, feature::*, inflation::*, nonce::*, program::*, spend_utils::*, stake::*, + validator_info::*, vote::*, }; -use bincode::serialize; -use bip39::{Language, Mnemonic, MnemonicType, Seed}; use clap::{value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; use log::*; use num_traits::FromPrimitive; -use serde_json::{self, json, Value}; +use serde_json::{self, Value}; use solana_account_decoder::{UiAccount, UiAccountEncoding}; -use solana_bpf_loader_program::{bpf_verifier, BPFError, ThisInstructionMeter}; use solana_clap_utils::{ self, commitment::commitment_arg_with_default, @@ -21,9 +18,7 @@ use solana_clap_utils::{ offline::*, }; use solana_cli_output::{ - display::{ - build_balance_message, new_spinner_progress_bar, println_name_value, println_transaction, - }, + display::{build_balance_message, println_name_value, println_transaction}, return_signers, CliAccount, CliSignature, OutputFormat, }; use solana_client::{ @@ -32,30 +27,22 @@ use solana_client::{ nonce_utils, rpc_client::RpcClient, rpc_config::{RpcLargestAccountsFilter, RpcSendTransactionConfig, RpcTransactionLogsFilter}, - rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, - rpc_response::{RpcKeyedAccount, RpcLeaderSchedule}, + rpc_response::RpcKeyedAccount, }; #[cfg(not(test))] use solana_faucet::faucet::request_airdrop_transaction; #[cfg(test)] use solana_faucet::faucet_mock::request_airdrop_transaction; -use solana_rbpf::vm::{Config, Executable}; use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ - account::Account, - bpf_loader, bpf_loader_deprecated, - bpf_loader_upgradeable::{self, UpgradeableLoaderState}, clock::{Epoch, Slot}, commitment_config::CommitmentConfig, decode_error::DecodeError, hash::Hash, - instruction::{Instruction, InstructionError}, - loader_instruction, + instruction::InstructionError, message::Message, - native_token::Sol, pubkey::{Pubkey, MAX_SEED_LEN}, - signature::{keypair_from_seed, Keypair, Signature, Signer, SignerError}, - signers::Signers, + signature::{Signature, Signer, SignerError}, system_instruction::{self, SystemError}, system_program, transaction::{Transaction, TransactionError}, @@ -67,13 +54,12 @@ use solana_stake_program::{ use solana_transaction_status::{EncodedTransaction, UiTransactionEncoding}; use solana_vote_program::vote_state::VoteAuthorize; use std::{ - cmp::min, collections::HashMap, error, fmt::Write as FmtWrite, fs::File, - io::{Read, Write}, - net::{IpAddr, SocketAddr, UdpSocket}, + io::Write, + net::{IpAddr, SocketAddr}, str::FromStr, sync::Arc, thread::sleep, @@ -82,7 +68,6 @@ use std::{ use thiserror::Error; use url::Url; -const DATA_CHUNK_SIZE: usize = 229; // Keep program chunks under PACKET_DATA_SIZE pub const DEFAULT_RPC_TIMEOUT_SECONDS: &str = "30"; #[derive(Debug, PartialEq)] @@ -187,26 +172,13 @@ pub enum CliCommand { lamports: u64, }, // Program Deployment - ProgramDeploy { + Deploy { program_location: String, - buffer: Option, + address: Option, use_deprecated_loader: bool, - use_upgradeable_loader: bool, - upgrade_authority: Option, - max_len: Option, allow_excessive_balance: bool, }, - ProgramUpgrade { - program_location: String, - program: Pubkey, - upgrade_authority: SignerIndex, - buffer: Option, - }, - SetProgramUpgradeAuthority { - program: Pubkey, - upgrade_authority: SignerIndex, - new_upgrade_authority: Option, - }, + Program(ProgramCliCommand), // Stake Commands CreateStakeAccount { stake_account: SignerIndex, @@ -633,77 +605,26 @@ pub fn parse_command( parse_withdraw_from_nonce_account(matches, default_signer, wallet_manager) } // Program Deployment - ("program-deploy", Some(matches)) => { + ("deploy", Some(matches)) => { + let (address_signer, _address) = signer_of(matches, "address_signer", wallet_manager)?; let mut signers = vec![default_signer.signer_from_path(matches, wallet_manager)?]; - let (buffer_signer, _address) = signer_of(matches, "buffer_signer", wallet_manager)?; - let buffer = buffer_signer.map(|signer| { + let address = address_signer.map(|signer| { signers.push(signer); 1 }); - let upgrade_authority = pubkey_of(matches, "upgrade_authority"); - let max_len = value_of(matches, "max_len"); - Ok(CliCommandInfo { - command: CliCommand::ProgramDeploy { + command: CliCommand::Deploy { program_location: matches.value_of("program_location").unwrap().to_string(), - buffer, + address, use_deprecated_loader: matches.is_present("use_deprecated_loader"), - use_upgradeable_loader: matches.is_present("use_upgradeable_loader"), - upgrade_authority, - max_len, allow_excessive_balance: matches.is_present("allow_excessive_balance"), }, signers, }) } - ("program-upgrade", Some(matches)) => { - let mut signers = vec![default_signer.signer_from_path(matches, wallet_manager)?]; - let (upgrade_authority_signer, _address) = - signer_of(matches, "upgrade_authority", wallet_manager)?; - let upgrade_authority = upgrade_authority_signer - .map(|signer| { - signers.push(signer); - 1 - }) - .unwrap(); - let (buffer_signer, _address) = signer_of(matches, "buffer_signer", wallet_manager)?; - let buffer = buffer_signer.map(|signer| { - signers.push(signer); - 2 - }); - let program = pubkey_of(matches, "program_id").unwrap(); - - Ok(CliCommandInfo { - command: CliCommand::ProgramUpgrade { - program_location: matches.value_of("program_location").unwrap().to_string(), - program, - upgrade_authority, - buffer, - }, - signers, - }) - } - ("program-set-upgrade-authority", Some(matches)) => { - let mut signers = vec![default_signer.signer_from_path(matches, wallet_manager)?]; - let (upgrade_authority_signer, _address) = - signer_of(matches, "upgrade_authority", wallet_manager)?; - let upgrade_authority = upgrade_authority_signer - .map(|signer| { - signers.push(signer); - 1 - }) - .unwrap(); - let program = pubkey_of(matches, "program_id").unwrap(); - let new_upgrade_authority = pubkey_of(matches, "new_upgrade_authority"); - Ok(CliCommandInfo { - command: CliCommand::SetProgramUpgradeAuthority { - program, - upgrade_authority, - new_upgrade_authority, - }, - signers, - }) + ("program", Some(matches)) => { + parse_program_subcommand(matches, default_signer, wallet_manager) } ("wait-for-max-stake", Some(matches)) => { let max_stake_percent = value_t_or_exit!(matches, "max_percent", f32); @@ -1113,730 +1034,6 @@ fn process_show_account( Ok(account_string) } -fn send_and_confirm_transactions_with_spinner( - rpc_client: &RpcClient, - mut transactions: Vec, - signer_keys: &T, - commitment: CommitmentConfig, - mut last_valid_slot: Slot, -) -> Result<(), Box> { - let progress_bar = new_spinner_progress_bar(); - let mut send_retries = 5; - let mut leader_schedule: Option = None; - let mut leader_schedule_epoch = 0; - let send_socket = UdpSocket::bind("0.0.0.0:0").unwrap(); - let cluster_nodes = rpc_client.get_cluster_nodes().ok(); - - loop { - let mut status_retries = 15; - - progress_bar.set_message("Finding leader node..."); - let epoch_info = rpc_client.get_epoch_info_with_commitment(commitment)?; - if epoch_info.epoch > leader_schedule_epoch || leader_schedule.is_none() { - leader_schedule = rpc_client - .get_leader_schedule_with_commitment(Some(epoch_info.absolute_slot), commitment)?; - leader_schedule_epoch = epoch_info.epoch; - } - let tpu_address = get_leader_tpu( - min(epoch_info.slot_index + 1, epoch_info.slots_in_epoch), - leader_schedule.as_ref(), - cluster_nodes.as_ref(), - ); - - // Send all transactions - let mut pending_transactions = HashMap::new(); - let num_transactions = transactions.len(); - for transaction in transactions { - if let Some(tpu_address) = tpu_address { - let wire_transaction = - serialize(&transaction).expect("serialization should succeed"); - send_transaction_tpu(&send_socket, &tpu_address, &wire_transaction); - } else { - let _result = rpc_client - .send_transaction_with_config( - &transaction, - RpcSendTransactionConfig { - preflight_commitment: Some(commitment.commitment), - ..RpcSendTransactionConfig::default() - }, - ) - .ok(); - } - pending_transactions.insert(transaction.signatures[0], transaction); - - progress_bar.set_message(&format!( - "[{}/{}] Total Transactions sent", - pending_transactions.len(), - num_transactions - )); - } - - // Collect statuses for all the transactions, drop those that are confirmed - while status_retries > 0 { - status_retries -= 1; - - progress_bar.set_message(&format!( - "[{}/{}] Transactions confirmed", - num_transactions - pending_transactions.len(), - num_transactions - )); - - let mut statuses = vec![]; - let pending_signatures = pending_transactions.keys().cloned().collect::>(); - for pending_signatures_chunk in - pending_signatures.chunks(MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS - 1) - { - statuses.extend( - rpc_client - .get_signature_statuses_with_history(pending_signatures_chunk)? - .value - .into_iter(), - ); - } - assert_eq!(statuses.len(), pending_signatures.len()); - - for (signature, status) in pending_signatures.into_iter().zip(statuses.into_iter()) { - if let Some(status) = status { - if status.confirmations.is_none() || status.confirmations.unwrap() > 1 { - let _ = pending_transactions.remove(&signature); - } - } - progress_bar.set_message(&format!( - "[{}/{}] Transactions confirmed", - num_transactions - pending_transactions.len(), - num_transactions - )); - } - - if pending_transactions.is_empty() { - return Ok(()); - } - - let slot = rpc_client.get_slot_with_commitment(commitment)?; - if slot > last_valid_slot { - break; - } - - if cfg!(not(test)) { - // Retry twice a second - sleep(Duration::from_millis(500)); - } - } - - if send_retries == 0 { - return Err("Transactions failed".into()); - } - send_retries -= 1; - - // Re-sign any failed transactions with a new blockhash and retry - let (blockhash, _fee_calculator, new_last_valid_slot) = rpc_client - .get_recent_blockhash_with_commitment(commitment)? - .value; - last_valid_slot = new_last_valid_slot; - transactions = vec![]; - for (_, mut transaction) in pending_transactions.into_iter() { - transaction.try_sign(signer_keys, blockhash)?; - transactions.push(transaction); - } - } -} - -fn read_and_verify_elf(program_location: &str) -> Result, Box> { - let mut file = File::open(program_location).map_err(|err| { - CliError::DynamicProgramError(format!("Unable to open program file: {}", err)) - })?; - let mut program_data = Vec::new(); - file.read_to_end(&mut program_data).map_err(|err| { - CliError::DynamicProgramError(format!("Unable to read program file: {}", err)) - })?; - - // Verify the program - Executable::::from_elf( - &program_data, - Some(|x| bpf_verifier::check(x, false)), - Config::default(), - ) - .map_err(|err| CliError::DynamicProgramError(format!("ELF error: {}", err)))?; - - Ok(program_data) -} - -fn complete_partial_program_init( - loader_id: &Pubkey, - payer_pubkey: &Pubkey, - elf_pubkey: &Pubkey, - account: &Account, - account_data_len: usize, - minimum_balance: u64, - allow_excessive_balance: bool, -) -> Result<(Vec, u64), Box> { - let mut instructions: Vec = vec![]; - let mut balance_needed = 0; - if account.executable { - return Err(CliError::DynamicProgramError( - "Buffer account is already executable".to_string(), - ) - .into()); - } - if account.owner != *loader_id && !system_program::check_id(&account.owner) { - return Err(CliError::DynamicProgramError( - "Buffer account is already owned by another account".to_string(), - ) - .into()); - } - - if account.data.is_empty() && system_program::check_id(&account.owner) { - instructions.push(system_instruction::allocate( - elf_pubkey, - account_data_len as u64, - )); - if account.owner != *loader_id { - instructions.push(system_instruction::assign(elf_pubkey, &loader_id)); - } - } - if account.lamports < minimum_balance { - let balance = minimum_balance - account.lamports; - instructions.push(system_instruction::transfer( - payer_pubkey, - elf_pubkey, - balance, - )); - balance_needed = balance; - } else if account.lamports > minimum_balance - && system_program::check_id(&account.owner) - && !allow_excessive_balance - { - return Err(CliError::DynamicProgramError(format!( - "Buffer account has a balance: {:?}; it may already be in use", - Sol(account.lamports) - )) - .into()); - } - Ok((instructions, balance_needed)) -} - -fn check_payer( - rpc_client: &RpcClient, - config: &CliConfig, - balance_needed: u64, - messages: &[&Message], -) -> Result<(), Box> { - let (_, fee_calculator, _) = rpc_client - .get_recent_blockhash_with_commitment(config.commitment)? - .value; - - // Does the payer have enough? - check_account_for_spend_multiple_fees_with_commitment( - rpc_client, - &config.signers[0].pubkey(), - balance_needed, - &fee_calculator, - messages, - config.commitment, - )?; - Ok(()) -} - -fn send_deploy_messages( - rpc_client: &RpcClient, - config: &CliConfig, - initial_message: &Option, - write_messages: &[Message], - final_message: &Message, - buffer_signer: &dyn Signer, - program_signer: &dyn Signer, -) -> Result<(), Box> { - if let Some(message) = initial_message { - trace!("Preparing the required accounts"); - - let (blockhash, _, _) = rpc_client - .get_recent_blockhash_with_commitment(config.commitment)? - .value; - - let mut initial_transaction = Transaction::new_unsigned(message.clone()); - // 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 - // extraneous signature. - if message.header.num_required_signatures == 2 { - initial_transaction.try_sign(&[config.signers[0], buffer_signer], blockhash)?; - } else { - initial_transaction.try_sign(&[config.signers[0]], blockhash)?; - } - let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config( - &initial_transaction, - config.commitment, - config.send_transaction_config, - ); - log_instruction_custom_error::(result, &config).map_err(|err| { - CliError::DynamicProgramError(format!("Account allocation failed: {}", err)) - })?; - } - - trace!("Writing program data"); - let (blockhash, _, last_valid_slot) = rpc_client - .get_recent_blockhash_with_commitment(config.commitment)? - .value; - let mut write_transactions = vec![]; - for message in write_messages.iter() { - let mut tx = Transaction::new_unsigned(message.clone()); - tx.try_sign(&[config.signers[0], buffer_signer], blockhash)?; - write_transactions.push(tx); - } - send_and_confirm_transactions_with_spinner( - &rpc_client, - write_transactions, - &[config.signers[0], buffer_signer], - config.commitment, - last_valid_slot, - ) - .map_err(|err| { - CliError::DynamicProgramError(format!("Data writes to account failed: {}", err)) - })?; - - trace!("Deploying program account"); - let (blockhash, _, _) = rpc_client - .get_recent_blockhash_with_commitment(config.commitment)? - .value; - - let mut final_tx = Transaction::new_unsigned(final_message.clone()); - final_tx.try_sign(&[config.signers[0], program_signer], blockhash)?; - rpc_client - .send_and_confirm_transaction_with_spinner_and_config( - &final_tx, - config.commitment, - RpcSendTransactionConfig { - skip_preflight: true, - preflight_commitment: Some(config.commitment.commitment), - ..RpcSendTransactionConfig::default() - }, - ) - .map_err(|e| { - CliError::DynamicProgramError(format!("Deploying program account failed: {}", e)) - })?; - Ok(()) -} - -fn process_program_deploy( - rpc_client: &RpcClient, - config: &CliConfig, - program_location: &str, - buffer: Option, - use_deprecated_loader: bool, - use_upgradeable_loader: bool, - upgrade_authority: Option, - max_len: Option, - allow_excessive_balance: bool, -) -> ProcessResult { - // Create ephemeral keypair to use for Buffer account, if not provided - let (words, mnemonic, new_keypair) = create_ephemeral_keypair()?; - let result = do_process_program_deploy( - rpc_client, - config, - program_location, - buffer, - use_deprecated_loader, - use_upgradeable_loader, - upgrade_authority, - max_len, - allow_excessive_balance, - new_keypair, - ); - if result.is_err() && buffer.is_none() { - report_ephemeral_mnemonic(words, mnemonic); - } - result -} - -#[allow(clippy::too_many_arguments)] -fn do_process_program_deploy( - rpc_client: &RpcClient, - config: &CliConfig, - program_location: &str, - buffer: Option, - use_deprecated_loader: bool, - use_upgradeable_loader: bool, - upgrade_authority: Option, - max_len: Option, - allow_excessive_balance: bool, - new_keypair: Keypair, -) -> ProcessResult { - let buffer_signer = if let Some(i) = buffer { - config.signers[i] - } else { - &new_keypair - }; - let program_keypair = Keypair::new(); - - let program_data = read_and_verify_elf(program_location)?; - - // Determine which loader - let (loader_id, program_signer, data_len, minimum_balance) = if use_deprecated_loader { - ( - bpf_loader_deprecated::id(), - buffer_signer, - program_data.len(), - rpc_client.get_minimum_balance_for_rent_exemption(program_data.len())?, - ) - } else if use_upgradeable_loader { - let data_len = if let Some(len) = max_len { - len - } else { - program_data.len() * 2 - }; - ( - bpf_loader_upgradeable::id(), - &program_keypair as &dyn Signer, - data_len, - rpc_client.get_minimum_balance_for_rent_exemption( - UpgradeableLoaderState::programdata_len(data_len)?, - )?, - ) - } else { - ( - bpf_loader::id(), - buffer_signer, - program_data.len(), - rpc_client.get_minimum_balance_for_rent_exemption(program_data.len())?, - ) - }; - - // Build messages to calculate fees - let mut messages: Vec<&Message> = Vec::new(); - - // Check Buffer account to see if partial initialization has occurred - let (initial_instructions, balance_needed) = if let Some(account) = rpc_client - .get_account_with_commitment(&buffer_signer.pubkey(), config.commitment)? - .value - { - let account_data_len = if loader_id == bpf_loader_upgradeable::id() { - UpgradeableLoaderState::buffer_len(data_len)? - } else { - data_len - }; - complete_partial_program_init( - &loader_id, - &config.signers[0].pubkey(), - &buffer_signer.pubkey(), - &account, - account_data_len, - minimum_balance, - allow_excessive_balance, - )? - } else if loader_id == bpf_loader_upgradeable::id() { - ( - bpf_loader_upgradeable::create_buffer( - &config.signers[0].pubkey(), - &buffer_signer.pubkey(), - minimum_balance, - data_len, - )?, - minimum_balance, - ) - } else { - ( - vec![system_instruction::create_account( - &config.signers[0].pubkey(), - &buffer_signer.pubkey(), - minimum_balance, - data_len as u64, - &loader_id, - )], - minimum_balance, - ) - }; - let initial_message = if !initial_instructions.is_empty() { - Some(Message::new( - &initial_instructions, - Some(&config.signers[0].pubkey()), - )) - } else { - None - }; - if let Some(message) = &initial_message { - messages.push(message); - } - - // Create and add write messages - let mut write_messages = vec![]; - for (chunk, i) in program_data.chunks(DATA_CHUNK_SIZE).zip(0..) { - let instruction = if loader_id == bpf_loader_upgradeable::id() { - bpf_loader_upgradeable::write( - &buffer_signer.pubkey(), - (i * DATA_CHUNK_SIZE) as u32, - chunk.to_vec(), - ) - } else { - loader_instruction::write( - &buffer_signer.pubkey(), - &loader_id, - (i * DATA_CHUNK_SIZE) as u32, - chunk.to_vec(), - ) - }; - let message = Message::new(&[instruction], Some(&config.signers[0].pubkey())); - write_messages.push(message); - } - let mut write_message_refs = vec![]; - for message in write_messages.iter() { - write_message_refs.push(message); - } - messages.append(&mut write_message_refs); - - // Create and add final message - let final_message = if loader_id == bpf_loader_upgradeable::id() { - Message::new( - &bpf_loader_upgradeable::deploy_with_max_program_len( - &config.signers[0].pubkey(), - &program_signer.pubkey(), - &buffer_signer.pubkey(), - upgrade_authority.as_ref(), - rpc_client.get_minimum_balance_for_rent_exemption( - UpgradeableLoaderState::program_len()?, - )?, - data_len, - )?, - Some(&config.signers[0].pubkey()), - ) - } else { - Message::new( - &[loader_instruction::finalize( - &buffer_signer.pubkey(), - &loader_id, - )], - Some(&config.signers[0].pubkey()), - ) - }; - messages.push(&final_message); - - check_payer(rpc_client, config, balance_needed, &messages)?; - send_deploy_messages( - rpc_client, - config, - &initial_message, - &write_messages, - &final_message, - buffer_signer, - program_signer, - )?; - - Ok(json!({ - "programId": format!("{}", program_signer.pubkey()), - }) - .to_string()) -} - -fn create_ephemeral_keypair( -) -> Result<(usize, bip39::Mnemonic, Keypair), Box> { - const WORDS: usize = 12; - let mnemonic = Mnemonic::new(MnemonicType::for_word_count(WORDS)?, Language::English); - let seed = Seed::new(&mnemonic, ""); - let new_keypair = keypair_from_seed(seed.as_bytes())?; - - Ok((WORDS, mnemonic, new_keypair)) -} - -fn report_ephemeral_mnemonic(words: usize, mnemonic: bip39::Mnemonic) { - let phrase: &str = mnemonic.phrase(); - let divider = String::from_utf8(vec![b'='; phrase.len()]).unwrap(); - eprintln!( - "{}\nTo resume a failed upgrade, recover the ephemeral keypair file with", - divider - ); - eprintln!( - "`solana-keygen recover` and the following {}-word seed phrase,", - words - ); - eprintln!( - "then pass it as the [BUFFER_SIGNER] argument to `solana upgrade ...`\n{}\n{}\n{}", - divider, phrase, divider - ); -} - -fn process_program_upgrade( - rpc_client: &RpcClient, - config: &CliConfig, - program_location: &str, - program: Pubkey, - upgrade_authority: SignerIndex, - buffer: Option, -) -> ProcessResult { - // Create ephemeral keypair to use for Buffer account, if not provided - let (words, mnemonic, new_keypair) = create_ephemeral_keypair()?; - let result = do_process_program_upgrade( - rpc_client, - config, - program_location, - program, - upgrade_authority, - buffer, - new_keypair, - ); - if result.is_err() && buffer.is_none() { - report_ephemeral_mnemonic(words, mnemonic); - } - result -} - -fn do_process_program_upgrade( - rpc_client: &RpcClient, - config: &CliConfig, - program_location: &str, - program: Pubkey, - upgrade_authority: SignerIndex, - buffer: Option, - new_keypair: Keypair, -) -> ProcessResult { - let buffer_signer = if let Some(i) = buffer { - config.signers[i] - } else { - &new_keypair - }; - let upgrade_authority = config.signers[upgrade_authority]; - - let program_data = read_and_verify_elf(program_location)?; - - let loader_id = bpf_loader_upgradeable::id(); - let data_len = program_data.len(); - let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption( - UpgradeableLoaderState::programdata_len(data_len)?, - )?; - - // Build messages to calculate fees - let mut messages: Vec<&Message> = Vec::new(); - - // Check Buffer account to see if partial initialization has occurred - let (initial_instructions, balance_needed) = if let Some(account) = rpc_client - .get_account_with_commitment(&buffer_signer.pubkey(), config.commitment)? - .value - { - complete_partial_program_init( - &loader_id, - &config.signers[0].pubkey(), - &buffer_signer.pubkey(), - &account, - UpgradeableLoaderState::buffer_len(data_len)?, - minimum_balance, - true, - )? - } else { - ( - bpf_loader_upgradeable::create_buffer( - &config.signers[0].pubkey(), - &buffer_signer.pubkey(), - minimum_balance, - data_len, - )?, - minimum_balance, - ) - }; - let initial_message = if !initial_instructions.is_empty() { - Some(Message::new( - &initial_instructions, - Some(&config.signers[0].pubkey()), - )) - } else { - None - }; - if let Some(message) = &initial_message { - messages.push(message); - } - - // Create and add write messages - let mut write_messages = vec![]; - for (chunk, i) in program_data.chunks(DATA_CHUNK_SIZE).zip(0..) { - let instruction = bpf_loader_upgradeable::write( - &buffer_signer.pubkey(), - (i * DATA_CHUNK_SIZE) as u32, - chunk.to_vec(), - ); - let message = Message::new(&[instruction], Some(&config.signers[0].pubkey())); - write_messages.push(message); - } - let mut write_message_refs = vec![]; - for message in write_messages.iter() { - write_message_refs.push(message); - } - messages.append(&mut write_message_refs); - - // Create and add final message - let final_message = Message::new( - &[bpf_loader_upgradeable::upgrade( - &program, - &buffer_signer.pubkey(), - &upgrade_authority.pubkey(), - &config.signers[0].pubkey(), - )], - Some(&config.signers[0].pubkey()), - ); - messages.push(&final_message); - - check_payer(rpc_client, config, balance_needed, &messages)?; - send_deploy_messages( - rpc_client, - config, - &initial_message, - &write_messages, - &final_message, - buffer_signer, - upgrade_authority, - )?; - - Ok(json!({ - "programId": format!("{}", program), - }) - .to_string()) -} - -fn process_set_program_upgrade_authority( - rpc_client: &RpcClient, - config: &CliConfig, - program: Pubkey, - upgrade_authority: SignerIndex, - new_upgrade_authority: Option, -) -> ProcessResult { - let upgrade_authority_signer = config.signers[upgrade_authority]; - - trace!("Set a new program upgrade authority"); - let (blockhash, _, _) = rpc_client - .get_recent_blockhash_with_commitment(config.commitment)? - .value; - - let mut tx = Transaction::new_unsigned(Message::new( - &[bpf_loader_upgradeable::set_authority( - &program, - &upgrade_authority_signer.pubkey(), - new_upgrade_authority.as_ref(), - )], - Some(&config.signers[0].pubkey()), - )); - tx.try_sign(&[config.signers[0], upgrade_authority_signer], blockhash)?; - rpc_client - .send_and_confirm_transaction_with_spinner_and_config( - &tx, - config.commitment, - RpcSendTransactionConfig { - skip_preflight: true, - preflight_commitment: Some(config.commitment.commitment), - ..RpcSendTransactionConfig::default() - }, - ) - .map_err(|e| { - CliError::DynamicProgramError(format!("Setting upgrade authority failed: {}", e)) - })?; - - match new_upgrade_authority { - Some(address) => Ok(json!({ - "UpgradeAuthority": format!("{:?}", address), - }) - .to_string()), - None => Ok(json!({ - "UpgradeAuthority": "None", - }) - .to_string()), - } -} - #[allow(clippy::too_many_arguments)] fn process_transfer( rpc_client: &RpcClient, @@ -2093,53 +1290,22 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { // Program Deployment // Deploy a custom program to the chain - CliCommand::ProgramDeploy { + CliCommand::Deploy { program_location, - buffer, + address, use_deprecated_loader, - use_upgradeable_loader, - upgrade_authority, - max_len, allow_excessive_balance, - } => process_program_deploy( + } => process_deploy( &rpc_client, config, program_location, - *buffer, + *address, *use_deprecated_loader, - *use_upgradeable_loader, - *upgrade_authority, - *max_len, *allow_excessive_balance, ), - - // Upgrade a deployed program on the chain - CliCommand::ProgramUpgrade { - program_location, - program, - upgrade_authority, - buffer, - } => process_program_upgrade( - &rpc_client, - config, - program_location, - *program, - *upgrade_authority, - *buffer, - ), - - // Set a new program upgrade authority - CliCommand::SetProgramUpgradeAuthority { - program, - upgrade_authority, - new_upgrade_authority, - } => process_set_program_upgrade_authority( - &rpc_client, - config, - *program, - *upgrade_authority, - *new_upgrade_authority, - ), + CliCommand::Program(program_subcommand) => { + process_program_subcommand(&rpc_client, config, program_subcommand) + } // Stake Commands @@ -2625,6 +1791,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .feature_subcommands() .inflation_subcommands() .nonce_subcommands() + .program_subcommands() .stake_subcommands() .subcommand( SubCommand::with_name("airdrop") @@ -2742,8 +1909,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' ), ) .subcommand( - SubCommand::with_name("program-deploy") - .visible_alias("deploy") + SubCommand::with_name("deploy") .about("Deploy a program") .arg( Arg::with_name("program_location") @@ -2751,117 +1917,29 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .value_name("PROGRAM_FILEPATH") .takes_value(true) .required(true) - .help("/path/to/program.so"), + .help("/path/to/program.o"), ) .arg( - Arg::with_name("buffer_signer") + Arg::with_name("address_signer") .index(2) - .value_name("BUFFER_SIGNER") + .value_name("PROGRAM_ADDRESS_SIGNER") .takes_value(true) .validator(is_valid_signer) - .help("The intermediate buffer account to write data to, can be used to resume a failed deploy [default: new random address]") + .help("The signer for the desired address of the program [default: new random address]") ) .arg( Arg::with_name("use_deprecated_loader") .long("use-deprecated-loader") .takes_value(false) .hidden(true) // Don't document this argument to discourage its use - .conflicts_with("upgradeable") .help("Use the deprecated BPF loader") ) - .arg( - Arg::with_name("use_upgradeable_loader") - .long("upgradeable") - .help("Use the upgradeable loader with an optional upgrade authority") - ) - .arg( - Arg::with_name("upgrade_authority") - .long("upgrade-authority") - .value_name("UPGRADE_AUTHORITY_PUBKEY") - .takes_value(true) - .help("Address of the upgrade authority, if not specified the program will not upgradeable") - ) - .arg( - Arg::with_name("max_len") - .long("max-len") - .value_name("max_len") - .takes_value(true) - .required(false) - .help("Maximum length of the upgradeable program, default's to twice the length of the original if not specified") - ) .arg( Arg::with_name("allow_excessive_balance") .long("allow-excessive-deploy-account-balance") .takes_value(false) .help("Use the designated program id, even if the account already holds a large balance of SOL") ) - .arg(commitment_arg_with_default("singleGossip")), - ) - .subcommand( - SubCommand::with_name("program-upgrade") - .about("Upgrade a program") - .arg( - Arg::with_name("program_id") - .index(1) - .value_name("PROGRAM_ADDRESS") - .takes_value(true) - .required(true) - .help("Public key of the program to upgrade") - ) - .arg( - Arg::with_name("upgrade_authority") - .index(2) - .value_name("AUTHORITY_SIGNER") - .takes_value(true) - .required(true) - .validator(is_valid_signer) - .help("Signer who has the authority to upgrade the program") - ) - .arg( - Arg::with_name("program_location") - .index(3) - .value_name("PROGRAM_FILEPATH") - .takes_value(true) - .required(true) - .help("/path/to/program.so"), - ) - .arg( - Arg::with_name("buffer_signer") - .index(4) - .value_name("BUFFER_SIGNER") - .takes_value(true) - .validator(is_valid_signer) - .help("The intermediate buffer account to write data to, can be used to resume a failed deploy [default: new random address]") - ) - .arg(commitment_arg_with_default("max")), - ) - .subcommand( - SubCommand::with_name("program-set-upgrade-authority") - .about("Set a new authority for an upgradeable program") - .arg( - Arg::with_name("program_id") - .index(1) - .value_name("PROGRAM_ADDRESS") - .takes_value(true) - .required(true) - .help("Public key of the program to upgrade") - ) - .arg( - Arg::with_name("upgrade_authority") - .index(2) - .value_name("AUTHORITY_SIGNER") - .takes_value(true) - .required(true) - .validator(is_valid_signer) - .help("Signer who has the authority to upgrade the program") - ) - .arg( - Arg::with_name("new_upgrade_authority") - .long("new-upgrade-authority") - .value_name("NEW_UPGRADE_AUTHORITY_PUBKEY") - .takes_value(true) - .help("Address of the upgrade authority, if not specified the program will not upgradeable") - ) .arg(commitment_arg_with_default("max")), ) .subcommand( @@ -2967,7 +2045,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' #[cfg(test)] mod tests { use super::*; - use serde_json::Value; + use serde_json::{json, Value}; use solana_client::{ blockhash_query, mock_sender::SIGNATURE, @@ -2976,7 +2054,7 @@ mod tests { }; use solana_sdk::{ pubkey::Pubkey, - signature::{keypair_from_seed, read_keypair_file, write_keypair_file, Presigner}, + signature::{keypair_from_seed, read_keypair_file, write_keypair_file, Keypair, Presigner}, transaction::TransactionError, }; use std::path::PathBuf; @@ -3219,22 +2297,18 @@ mod tests { ); // Test Deploy Subcommand - let test_deploy = test_commands.clone().get_matches_from(vec![ - "test", - "deploy", - "/Users/test/program.so", - ]); + let test_deploy = + test_commands + .clone() + .get_matches_from(vec!["test", "deploy", "/Users/test/program.o"]); assert_eq!( parse_command(&test_deploy, &default_signer, &mut None).unwrap(), CliCommandInfo { - command: CliCommand::ProgramDeploy { - program_location: "/Users/test/program.so".to_string(), - buffer: None, + command: CliCommand::Deploy { + program_location: "/Users/test/program.o".to_string(), + address: None, use_deprecated_loader: false, - use_upgradeable_loader: false, allow_excessive_balance: false, - upgrade_authority: None, - max_len: None, }, signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } @@ -3246,20 +2320,17 @@ mod tests { let test_deploy = test_commands.clone().get_matches_from(vec![ "test", "deploy", - "/Users/test/program.so", + "/Users/test/program.o", &custom_address_file, ]); assert_eq!( parse_command(&test_deploy, &default_signer, &mut None).unwrap(), CliCommandInfo { - command: CliCommand::ProgramDeploy { - program_location: "/Users/test/program.so".to_string(), - buffer: Some(1), + command: CliCommand::Deploy { + program_location: "/Users/test/program.o".to_string(), + address: Some(1), use_deprecated_loader: false, - use_upgradeable_loader: false, allow_excessive_balance: false, - upgrade_authority: None, - max_len: None, }, signers: vec![ read_keypair_file(&keypair_file).unwrap().into(), @@ -3268,143 +2339,6 @@ mod tests { } ); - let custom_address = Keypair::new(); - let custom_address_file = make_tmp_path("custom_address_file"); - write_keypair_file(&custom_address, &custom_address_file).unwrap(); - let test_deploy = test_commands.clone().get_matches_from(vec![ - "test", - "program-deploy", - "/Users/test/program.so", - &custom_address_file, - ]); - assert_eq!( - parse_command(&test_deploy, &default_signer, &mut None).unwrap(), - CliCommandInfo { - command: CliCommand::ProgramDeploy { - program_location: "/Users/test/program.so".to_string(), - buffer: Some(1), - use_deprecated_loader: false, - use_upgradeable_loader: false, - allow_excessive_balance: false, - upgrade_authority: None, - max_len: None, - }, - signers: vec![ - read_keypair_file(&keypair_file).unwrap().into(), - read_keypair_file(&custom_address_file).unwrap().into(), - ], - } - ); - - let upgrade_authority = Pubkey::new_unique(); - let test_deploy = test_commands.clone().get_matches_from(vec![ - "test", - "program-deploy", - "/Users/test/program.so", - "--upgradeable", - "--upgrade-authority", - &upgrade_authority.to_string(), - "--max-len", - "42", - ]); - assert_eq!( - parse_command(&test_deploy, &default_signer, &mut None).unwrap(), - CliCommandInfo { - command: CliCommand::ProgramDeploy { - program_location: "/Users/test/program.so".to_string(), - buffer: None, - use_deprecated_loader: false, - use_upgradeable_loader: true, - allow_excessive_balance: false, - upgrade_authority: Some(upgrade_authority), - max_len: Some(42), - }, - signers: vec![read_keypair_file(&keypair_file).unwrap().into()], - } - ); - - // Test Upgrade Subcommand - let program_pubkey = Pubkey::new_unique(); - let upgrade_address = Keypair::new(); - let upgrade_address_file = make_tmp_path("upgrade_address_file"); - write_keypair_file(&upgrade_address, &upgrade_address_file).unwrap(); - let test = test_commands.clone().get_matches_from(vec![ - "test", - "program-upgrade", - &program_pubkey.to_string(), - &upgrade_address_file, - "/Users/test/program.so", - ]); - assert_eq!( - parse_command(&test, &default_signer, &mut None).unwrap(), - CliCommandInfo { - command: CliCommand::ProgramUpgrade { - program_location: "/Users/test/program.so".to_string(), - program: program_pubkey, - buffer: None, - upgrade_authority: 1, - }, - signers: vec![ - read_keypair_file(&keypair_file).unwrap().into(), - read_keypair_file(&upgrade_address_file).unwrap().into() - ], - } - ); - - // Test SetProgramUpgradeAuthority Subcommand - let new_upgrade_authority = Pubkey::new_unique(); - let program_pubkey = Pubkey::new_unique(); - let authority_address = Keypair::new(); - let authority_address_file = make_tmp_path("authority_address_file"); - write_keypair_file(&authority_address, &authority_address_file).unwrap(); - let test = test_commands.clone().get_matches_from(vec![ - "test", - "program-set-upgrade-authority", - &program_pubkey.to_string(), - &authority_address_file, - "--new-upgrade-authority", - &new_upgrade_authority.to_string(), - ]); - assert_eq!( - parse_command(&test, &default_signer, &mut None).unwrap(), - CliCommandInfo { - command: CliCommand::SetProgramUpgradeAuthority { - program: program_pubkey, - upgrade_authority: 1, - new_upgrade_authority: Some(new_upgrade_authority), - }, - signers: vec![ - read_keypair_file(&keypair_file).unwrap().into(), - read_keypair_file(&authority_address_file).unwrap().into() - ], - } - ); - - let program_pubkey = Pubkey::new_unique(); - let authority_address = Keypair::new(); - let authority_address_file = make_tmp_path("authority_address_file"); - write_keypair_file(&authority_address, &authority_address_file).unwrap(); - let test = test_commands.clone().get_matches_from(vec![ - "test", - "program-set-upgrade-authority", - &program_pubkey.to_string(), - &authority_address_file, - ]); - assert_eq!( - parse_command(&test, &default_signer, &mut None).unwrap(), - CliCommandInfo { - command: CliCommand::SetProgramUpgradeAuthority { - program: program_pubkey, - upgrade_authority: 1, - new_upgrade_authority: None, - }, - signers: vec![ - read_keypair_file(&keypair_file).unwrap().into(), - read_keypair_file(&authority_address_file).unwrap().into() - ], - } - ); - // Test ResolveSigner Subcommand, KeypairUrl::Filepath let test_resolve_signer = test_commands @@ -3708,14 +2642,11 @@ mod tests { let default_keypair = Keypair::new(); config.signers = vec![&default_keypair]; - config.command = CliCommand::ProgramDeploy { + config.command = CliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - buffer: None, + address: None, use_deprecated_loader: false, - use_upgradeable_loader: false, allow_excessive_balance: false, - upgrade_authority: None, - max_len: None, }; let result = process_command(&config); let json: Value = serde_json::from_str(&result.unwrap()).unwrap(); @@ -3730,192 +2661,15 @@ mod tests { assert!(program_id.parse::().is_ok()); // Failure case - config.command = CliCommand::ProgramDeploy { + config.command = CliCommand::Deploy { program_location: "bad/file/location.so".to_string(), - buffer: None, + address: None, use_deprecated_loader: false, - use_upgradeable_loader: false, allow_excessive_balance: false, - upgrade_authority: None, - max_len: None, }; assert!(process_command(&config).is_err()); } - #[test] - fn test_cli_deploy_upgradeable() { - solana_logger::setup(); - - let upgrade_authority = Pubkey::new_unique(); - let mut pathbuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - pathbuf.push("tests"); - pathbuf.push("fixtures"); - pathbuf.push("noop"); - pathbuf.set_extension("so"); - - // Success case - let mut config = CliConfig::default(); - let account_info_response = json!(Response { - context: RpcResponseContext { slot: 1 }, - value: Value::Null, - }); - let mut mocks = HashMap::new(); - mocks.insert(RpcRequest::GetAccountInfo, account_info_response); - let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); - - config.rpc_client = Some(rpc_client); - let default_keypair = Keypair::new(); - config.signers = vec![&default_keypair]; - - config.command = CliCommand::ProgramDeploy { - program_location: pathbuf.to_str().unwrap().to_string(), - buffer: None, - use_deprecated_loader: false, - use_upgradeable_loader: true, - allow_excessive_balance: false, - upgrade_authority: Some(upgrade_authority), - max_len: None, - }; - let result = process_command(&config); - let json: Value = serde_json::from_str(&result.unwrap()).unwrap(); - let program_id = json - .as_object() - .unwrap() - .get("programId") - .unwrap() - .as_str() - .unwrap(); - - assert!(program_id.parse::().is_ok()); - - // Failure case - config.command = CliCommand::ProgramDeploy { - program_location: "bad/file/location.so".to_string(), - buffer: None, - use_deprecated_loader: false, - use_upgradeable_loader: true, - allow_excessive_balance: false, - upgrade_authority: Some(upgrade_authority), - max_len: None, - }; - assert!(process_command(&config).is_err()); - } - - #[test] - fn test_cli_upgrade() { - solana_logger::setup(); - - let upgrade_authority = Keypair::new(); - let program = Pubkey::new_unique(); - let mut pathbuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - pathbuf.push("tests"); - pathbuf.push("fixtures"); - pathbuf.push("noop"); - pathbuf.set_extension("so"); - - // Success case - let mut config = CliConfig::default(); - let account_info_response = json!(Response { - context: RpcResponseContext { slot: 1 }, - value: Value::Null, - }); - let mut mocks = HashMap::new(); - mocks.insert(RpcRequest::GetAccountInfo, account_info_response); - let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); - - config.rpc_client = Some(rpc_client); - let default_keypair = Keypair::new(); - config.signers = vec![&default_keypair, &upgrade_authority]; - - config.command = CliCommand::ProgramUpgrade { - program_location: pathbuf.to_str().unwrap().to_string(), - program, - buffer: None, - upgrade_authority: 1, - }; - let result = process_command(&config); - let json: Value = serde_json::from_str(&result.unwrap()).unwrap(); - let program_id = json - .as_object() - .unwrap() - .get("programId") - .unwrap() - .as_str() - .unwrap(); - - assert!(program_id.parse::().is_ok()); - - // Failure case - config.command = CliCommand::ProgramUpgrade { - program_location: "bad/file/location.so".to_string(), - program, - buffer: None, - upgrade_authority: 1, - }; - assert!(process_command(&config).is_err()); - } - - #[test] - fn test_cli_set_upgrade_authority() { - solana_logger::setup(); - - let program = Pubkey::new_unique(); - let upgrade_authority = Keypair::new(); - let new_upgrade_authority = Pubkey::new_unique(); - let mut pathbuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - pathbuf.push("tests"); - pathbuf.push("fixtures"); - pathbuf.push("noop"); - pathbuf.set_extension("so"); - - // Success case - let mut config = CliConfig::default(); - let account_info_response = json!(Response { - context: RpcResponseContext { slot: 1 }, - value: Value::Null, - }); - let mut mocks = HashMap::new(); - mocks.insert(RpcRequest::GetAccountInfo, account_info_response); - let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); - - config.rpc_client = Some(rpc_client); - let default_keypair = Keypair::new(); - config.signers = vec![&default_keypair, &upgrade_authority]; - - config.command = CliCommand::SetProgramUpgradeAuthority { - program, - upgrade_authority: 1, - new_upgrade_authority: Some(new_upgrade_authority), - }; - let result = process_command(&config); - let json: Value = serde_json::from_str(&result.unwrap()).unwrap(); - println!("json {:?}", json); - let program_id = json - .as_object() - .unwrap() - .get("UpgradeAuthority") - .unwrap() - .as_str() - .unwrap(); - assert!(program_id.parse::().is_ok()); - - config.command = CliCommand::SetProgramUpgradeAuthority { - program, - upgrade_authority: 1, - new_upgrade_authority: None, - }; - let result = process_command(&config); - let json: Value = serde_json::from_str(&result.unwrap()).unwrap(); - let program_id = json - .as_object() - .unwrap() - .get("UpgradeAuthority") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(program_id, "None"); - } - #[test] fn test_parse_transfer_subcommand() { let test_commands = app("test", "desc", "version"); diff --git a/cli/src/lib.rs b/cli/src/lib.rs index e735fa3a46..eb4baa2f02 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -26,6 +26,7 @@ pub mod cluster_query; pub mod feature; pub mod inflation; pub mod nonce; +pub mod program; pub mod send_tpu; pub mod spend_utils; pub mod stake; diff --git a/cli/src/program.rs b/cli/src/program.rs new file mode 100644 index 0000000000..968ce67084 --- /dev/null +++ b/cli/src/program.rs @@ -0,0 +1,1537 @@ +use crate::send_tpu::{get_leader_tpu, send_transaction_tpu}; +use crate::{ + checks::*, + cli::{ + log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, + ProcessResult, + }, +}; +use bincode::serialize; +use bip39::{Language, Mnemonic, MnemonicType, Seed}; +use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; +use log::*; +use serde_json::{self, json}; +use solana_bpf_loader_program::{bpf_verifier, BPFError, ThisInstructionMeter}; +use solana_clap_utils::{ + self, commitment::commitment_arg_with_default, input_parsers::*, input_validators::*, + keypair::*, +}; +use solana_cli_output::display::new_spinner_progress_bar; +use solana_client::{ + rpc_client::RpcClient, rpc_config::RpcSendTransactionConfig, + rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, rpc_response::RpcLeaderSchedule, +}; +use solana_rbpf::vm::{Config, Executable}; +use solana_remote_wallet::remote_wallet::RemoteWalletManager; +use solana_sdk::{ + account::Account, + account_utils::StateMut, + bpf_loader, bpf_loader_deprecated, + bpf_loader_upgradeable::{self, UpgradeableLoaderState}, + clock::Slot, + commitment_config::CommitmentConfig, + instruction::Instruction, + loader_instruction, + message::Message, + native_token::Sol, + pubkey::Pubkey, + signature::{keypair_from_seed, read_keypair_file, Keypair, Signer}, + signers::Signers, + system_instruction::{self, SystemError}, + system_program, + transaction::Transaction, +}; +use std::{ + cmp::min, collections::HashMap, error, fs::File, io::Read, net::UdpSocket, path::PathBuf, + sync::Arc, thread::sleep, time::Duration, +}; + +const DATA_CHUNK_SIZE: usize = 229; // Keep program chunks under PACKET_DATA_SIZE + +#[derive(Debug, PartialEq)] +pub enum ProgramCliCommand { + Deploy { + program_location: String, + program_signer_index: Option, + program_pubkey: Option, + buffer_signer_index: Option, + upgrade_authority_signer_index: Option, + upgrade_authority_pubkey: Option, + max_len: Option, + allow_excessive_balance: bool, + }, + SetUpgradeAuthority { + program: Pubkey, + upgrade_authority_index: Option, + new_upgrade_authority: Option, + }, +} + +pub trait ProgramSubCommands { + fn program_subcommands(self) -> Self; +} + +impl ProgramSubCommands for App<'_, '_> { + fn program_subcommands(self) -> Self { + self.subcommand( + SubCommand::with_name("program") + .about("Program management") + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + SubCommand::with_name("deploy") + .about("Deploy a program") + .arg( + Arg::with_name("program_location") + .index(1) + .value_name("PROGRAM_FILEPATH") + .takes_value(true) + .required(true) + .help("/path/to/program.so"), + ) + .arg( + Arg::with_name("buffer") + .long("buffer") + .value_name("BUFFER_SIGNER") + .takes_value(true) + .validator(is_valid_signer) + .help("Intermediate buffer account to write data to, can be used to resume a failed deploy [default: new random address]") + ) + .arg( + pubkey!(Arg::with_name("upgrade_authority") + .long("upgrade-authority") + .value_name("UPGRADE_AUTHORITY"), + "Upgrade authority [default: the default configured keypair]"), + ) + .arg( + pubkey!(Arg::with_name("program_id") + .long("program-id") + .value_name("PROGRAM_ID"), + "Executable program's address, must be a signer for initial deploys, can be a pubkey for upgrades [default: new random address or the address of keypair file /path/to/program.json]"), + ) + .arg( + Arg::with_name("final") + .long("final") + .help("The program will not be upgradeable") + ) + .arg( + Arg::with_name("max_len") + .long("max-len") + .value_name("max_len") + .takes_value(true) + .required(false) + .help("Maximum length of the upgradeable program, [default: twice the length of the original deployed program]") + ) + .arg( + Arg::with_name("allow_excessive_balance") + .long("allow-excessive-deploy-account-balance") + .takes_value(false) + .help("Use the designated program id, even if the account already holds a large balance of SOL") + ) + .arg(commitment_arg_with_default("singleGossip")), + ) + .subcommand( + SubCommand::with_name("set-upgrade-authority") + .about("Set a new program authority") + .arg( + Arg::with_name("program_id") + .index(1) + .value_name("PROGRAM_ADDRESS") + .takes_value(true) + .required(true) + .help("Public key of the program to upgrade") + ) + .arg( + Arg::with_name("upgrade_authority") + .long("upgrade-authority") + .value_name("UPGRADE_AUTHORITY_SIGNER") + .takes_value(true) + .validator(is_valid_signer) + .help("Upgrade authority [default: the default configured keypair]") + ) + .arg( + pubkey!(Arg::with_name("new_upgrade_authority") + .long("new-upgrade-authority") + .required_unless("final") + .value_name("NEW_UPGRADE_AUTHORITY"), + "Address of the upgrade authority, if not specified the program will not be upgradeable"), + ) + .arg( + Arg::with_name("final") + .long("final") + .conflicts_with("new_upgrade_authority") + .help("The program will not be upgradeable") + ) + .arg(commitment_arg_with_default("max")), + ) + ) + } +} + +pub fn parse_program_subcommand( + matches: &ArgMatches<'_>, + default_signer: &DefaultSigner, + wallet_manager: &mut Option>, +) -> Result { + let response = match matches.subcommand() { + ("deploy", Some(matches)) => { + let mut bulk_signers = vec![Some( + default_signer.signer_from_path(matches, wallet_manager)?, + )]; + + let (buffer_signer, buffer_pubkey) = signer_of(matches, "buffer", wallet_manager)?; + bulk_signers.push(buffer_signer); + + let program_pubkey = if let Ok((program_signer, Some(program_pubkey))) = + signer_of(matches, "program_id", wallet_manager) + { + bulk_signers.push(program_signer); + Some(program_pubkey) + } else if let Some(program_pubkey) = + pubkey_of_signer(matches, "program_id", wallet_manager)? + { + Some(program_pubkey) + } else { + None + }; + + let upgrade_authority_pubkey = if matches.is_present("final") { + None + } else if let Ok((upgrade_authority_signer, Some(upgrade_authority_pubkey))) = + signer_of(matches, "upgrade_authority", wallet_manager) + { + bulk_signers.push(upgrade_authority_signer); + Some(upgrade_authority_pubkey) + } else if let Some(upgrade_authority_pubkey) = + pubkey_of_signer(matches, "upgrade_authority", wallet_manager)? + { + Some(upgrade_authority_pubkey) + } else { + Some( + default_signer + .signer_from_path(matches, wallet_manager)? + .pubkey(), + ) + }; + + let max_len = value_of(matches, "max_len"); + + let signer_info = + default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; + + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Deploy { + program_location: matches.value_of("program_location").unwrap().to_string(), + program_signer_index: signer_info.index_of_or_none(program_pubkey), + program_pubkey, + buffer_signer_index: signer_info.index_of_or_none(buffer_pubkey), + upgrade_authority_signer_index: signer_info + .index_of_or_none(upgrade_authority_pubkey), + upgrade_authority_pubkey, + max_len, + allow_excessive_balance: matches.is_present("allow_excessive_balance"), + }), + signers: signer_info.signers, + } + } + ("set-upgrade-authority", Some(matches)) => { + let (upgrade_authority_signer, upgrade_authority_pubkey) = + signer_of(matches, "upgrade_authority", wallet_manager)?; + let program = pubkey_of(matches, "program_id").unwrap(); + let new_upgrade_authority = if matches.is_present("final") { + None + } else if let Some(new_upgrade_authority) = + pubkey_of_signer(matches, "new_upgrade_authority", wallet_manager)? + { + Some(new_upgrade_authority) + } else { + let (_, new_upgrade_authority) = + signer_of(matches, "new_upgrade_authority", wallet_manager)?; + new_upgrade_authority + }; + + let signer_info = default_signer.generate_unique_signers( + vec![ + Some(default_signer.signer_from_path(matches, wallet_manager)?), + upgrade_authority_signer, + ], + matches, + wallet_manager, + )?; + + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::SetUpgradeAuthority { + program, + upgrade_authority_index: signer_info.index_of(upgrade_authority_pubkey), + new_upgrade_authority, + }), + signers: signer_info.signers, + } + } + _ => unreachable!(), + }; + Ok(response) +} + +pub fn process_program_subcommand( + rpc_client: &RpcClient, + config: &CliConfig, + program_subcommand: &ProgramCliCommand, +) -> ProcessResult { + match program_subcommand { + ProgramCliCommand::Deploy { + program_location, + program_signer_index, + program_pubkey, + buffer_signer_index, + upgrade_authority_signer_index, + upgrade_authority_pubkey, + max_len, + allow_excessive_balance, + } => process_program_deploy( + &rpc_client, + config, + program_location, + *program_signer_index, + *program_pubkey, + *buffer_signer_index, + *upgrade_authority_signer_index, + *upgrade_authority_pubkey, + *max_len, + *allow_excessive_balance, + ), + ProgramCliCommand::SetUpgradeAuthority { + program, + upgrade_authority_index, + new_upgrade_authority, + } => process_set_program_upgrade_authority( + &rpc_client, + config, + *program, + *upgrade_authority_index, + *new_upgrade_authority, + ), + } +} + +fn get_default_program_keypair(program_location: &str) -> Keypair { + let program_keypair = { + let mut keypair_file = PathBuf::new(); + keypair_file.push(program_location); + let mut filename = keypair_file.file_stem().unwrap().to_os_string(); + filename.push("-keypair"); + keypair_file.set_file_name(filename); + keypair_file.set_extension("json"); + if let Ok(keypair) = read_keypair_file(&keypair_file.to_str().unwrap()) { + keypair + } else { + Keypair::new() + } + }; + program_keypair +} + +/// Deploy using upgradeable loader +#[allow(clippy::too_many_arguments)] +fn process_program_deploy( + rpc_client: &RpcClient, + config: &CliConfig, + program_location: &str, + program_signer_index: Option, + program_pubkey: Option, + buffer_signer_index: Option, + upgrade_authority_signer_index: Option, + upgrade_authority: Option, + max_len: Option, + allow_excessive_balance: bool, +) -> ProcessResult { + // Create ephemeral keypair to use for Buffer account, if not provided + let (words, mnemonic, buffer_keypair) = create_ephemeral_keypair()?; + let buffer_signer = if let Some(i) = buffer_signer_index { + config.signers[i] + } else { + &buffer_keypair + }; + + let default_program_keypair = get_default_program_keypair(program_location); + let (program_signer, program_id) = if let Some(i) = program_signer_index { + (Some(config.signers[i]), config.signers[i].pubkey()) + } else if let Some(program_id) = program_pubkey { + (None, program_id) + } else { + ( + Some(&default_program_keypair as &dyn Signer), + default_program_keypair.pubkey(), + ) + }; + + let do_deploy = if let Some(account) = rpc_client + .get_account_with_commitment(&program_id, config.commitment)? + .value + { + if !account.executable { + // Continue an initial deploy + true + } else if let UpgradeableLoaderState::Program { + programdata_address, + } = account.state()? + { + if let Some(account) = rpc_client + .get_account_with_commitment(&programdata_address, config.commitment)? + .value + { + if let UpgradeableLoaderState::ProgramData { + slot: _, + upgrade_authority_address: program_authority, + } = account.state()? + { + if program_authority.is_none() { + return Err("Program is no longer upgradeable".into()); + } + if program_authority != upgrade_authority { + return Err(format!( + "Program's authority {:?} does not match authority provided {:?}", + program_authority, upgrade_authority, + ) + .into()); + } + // Do upgrade + false + } else { + return Err("Program account is corrupt".into()); + } + } else { + return Err("Program account is corrupt".into()); + } + } else { + return Err(format!("Program {:?} is not an upgradeable program", program_id).into()); + } + } else { + // do new deploy + true + }; + + let program_data = read_and_verify_elf(program_location)?; + let buffer_data_len = if let Some(len) = max_len { + len + } else { + program_data.len() * 2 + }; + let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption( + UpgradeableLoaderState::programdata_len(buffer_data_len)?, + )?; + + let result = if do_deploy { + do_process_program_deploy( + rpc_client, + config, + &program_data, + buffer_data_len, + minimum_balance, + &bpf_loader_upgradeable::id(), + program_signer.unwrap(), + buffer_signer, + upgrade_authority, + allow_excessive_balance, + ) + } else if let Some(upgrade_authority_index) = upgrade_authority_signer_index { + do_process_program_upgrade( + rpc_client, + config, + &program_data, + program_id, + config.signers[upgrade_authority_index], + buffer_signer, + ) + } else { + return Err("Program upgrade requires an authority".into()); + }; + if result.is_err() && buffer_signer_index.is_none() { + report_ephemeral_mnemonic(words, mnemonic); + } + result +} + +fn process_set_program_upgrade_authority( + rpc_client: &RpcClient, + config: &CliConfig, + program_id: Pubkey, + upgrade_authority: Option, + new_upgrade_authority: Option, +) -> ProcessResult { + let upgrade_authority_signer = if let Some(index) = upgrade_authority { + config.signers[index] + } else { + return Err("Set authority requires the current authority".into()); + }; + + trace!("Set a new program upgrade authority"); + let (blockhash, _, _) = rpc_client + .get_recent_blockhash_with_commitment(config.commitment)? + .value; + + let mut tx = Transaction::new_unsigned(Message::new( + &[bpf_loader_upgradeable::set_authority( + &program_id, + &upgrade_authority_signer.pubkey(), + new_upgrade_authority.as_ref(), + )], + Some(&config.signers[0].pubkey()), + )); + tx.try_sign(&[config.signers[0], upgrade_authority_signer], blockhash)?; + rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + config.commitment, + RpcSendTransactionConfig { + skip_preflight: true, + preflight_commitment: Some(config.commitment.commitment), + ..RpcSendTransactionConfig::default() + }, + ) + .map_err(|e| format!("Setting upgrade authority failed: {}", e))?; + + match new_upgrade_authority { + Some(address) => Ok(json!({ + "UpgradeAuthority": format!("{:?}", address), + }) + .to_string()), + None => Ok(json!({ + "UpgradeAuthority": "None", + }) + .to_string()), + } +} + +/// Deploy using non-upgradeable loader +pub fn process_deploy( + rpc_client: &RpcClient, + config: &CliConfig, + program_location: &str, + buffer_signer_index: Option, + use_deprecated_loader: bool, + allow_excessive_balance: bool, +) -> ProcessResult { + // Create ephemeral keypair to use for Buffer account, if not provided + let (words, mnemonic, buffer_keypair) = create_ephemeral_keypair()?; + let buffer_signer = if let Some(i) = buffer_signer_index { + config.signers[i] + } else { + &buffer_keypair + }; + + let program_data = read_and_verify_elf(program_location)?; + let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(program_data.len())?; + let loader_id = if use_deprecated_loader { + bpf_loader_deprecated::id() + } else { + bpf_loader::id() + }; + + let result = do_process_program_deploy( + rpc_client, + config, + &program_data, + program_data.len(), + minimum_balance, + &loader_id, + buffer_signer, + buffer_signer, + None, + allow_excessive_balance, + ); + if result.is_err() && buffer_signer_index.is_none() { + report_ephemeral_mnemonic(words, mnemonic); + } + result +} + +#[allow(clippy::too_many_arguments)] +fn do_process_program_deploy( + rpc_client: &RpcClient, + config: &CliConfig, + program_data: &[u8], + buffer_data_len: usize, + minimum_balance: u64, + loader_id: &Pubkey, + program_signer: &dyn Signer, + buffer_signer: &dyn Signer, + upgrade_authority: Option, + allow_excessive_balance: bool, +) -> ProcessResult { + // Build messages to calculate fees + let mut messages: Vec<&Message> = Vec::new(); + + // Initialize buffer account or complete if already partially initialized + + let (initial_instructions, balance_needed) = if let Some(account) = rpc_client + .get_account_with_commitment(&buffer_signer.pubkey(), config.commitment)? + .value + { + complete_partial_program_init( + &loader_id, + &config.signers[0].pubkey(), + &buffer_signer.pubkey(), + &account, + if loader_id == &bpf_loader_upgradeable::id() { + UpgradeableLoaderState::buffer_len(buffer_data_len)? + } else { + buffer_data_len + }, + minimum_balance, + allow_excessive_balance, + )? + } else if loader_id == &bpf_loader_upgradeable::id() { + ( + bpf_loader_upgradeable::create_buffer( + &config.signers[0].pubkey(), + &buffer_signer.pubkey(), + minimum_balance, + buffer_data_len, + )?, + minimum_balance, + ) + } else { + ( + vec![system_instruction::create_account( + &config.signers[0].pubkey(), + &buffer_signer.pubkey(), + minimum_balance, + buffer_data_len as u64, + &loader_id, + )], + minimum_balance, + ) + }; + + let initial_message = if !initial_instructions.is_empty() { + Some(Message::new( + &initial_instructions, + Some(&config.signers[0].pubkey()), + )) + } else { + None + }; + if let Some(message) = &initial_message { + messages.push(message); + } + + // Create and add write messages + + let mut write_messages = vec![]; + for (chunk, i) in program_data.chunks(DATA_CHUNK_SIZE).zip(0..) { + let instruction = if loader_id == &bpf_loader_upgradeable::id() { + bpf_loader_upgradeable::write( + &buffer_signer.pubkey(), + (i * DATA_CHUNK_SIZE) as u32, + chunk.to_vec(), + ) + } else { + loader_instruction::write( + &buffer_signer.pubkey(), + &loader_id, + (i * DATA_CHUNK_SIZE) as u32, + chunk.to_vec(), + ) + }; + let message = Message::new(&[instruction], Some(&config.signers[0].pubkey())); + write_messages.push(message); + } + let mut write_message_refs = vec![]; + for message in write_messages.iter() { + write_message_refs.push(message); + } + messages.append(&mut write_message_refs); + + // Create and add final message + + let final_message = if loader_id == &bpf_loader_upgradeable::id() { + Message::new( + &bpf_loader_upgradeable::deploy_with_max_program_len( + &config.signers[0].pubkey(), + &program_signer.pubkey(), + &buffer_signer.pubkey(), + upgrade_authority.as_ref(), + rpc_client.get_minimum_balance_for_rent_exemption( + UpgradeableLoaderState::program_len()?, + )?, + buffer_data_len, + )?, + Some(&config.signers[0].pubkey()), + ) + } else { + Message::new( + &[loader_instruction::finalize( + &buffer_signer.pubkey(), + &loader_id, + )], + Some(&config.signers[0].pubkey()), + ) + }; + messages.push(&final_message); + + check_payer(rpc_client, config, balance_needed, &messages)?; + + send_deploy_messages( + rpc_client, + config, + &initial_message, + &write_messages, + &final_message, + buffer_signer, + program_signer, + )?; + + Ok(json!({ + "programId": format!("{}", program_signer.pubkey()), + }) + .to_string()) +} + +fn do_process_program_upgrade( + rpc_client: &RpcClient, + config: &CliConfig, + program_data: &[u8], + program_id: Pubkey, + upgrade_authority: &dyn Signer, + buffer_signer: &dyn Signer, +) -> ProcessResult { + let loader_id = bpf_loader_upgradeable::id(); + let data_len = program_data.len(); + let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption( + UpgradeableLoaderState::programdata_len(data_len)?, + )?; + + // Build messages to calculate fees + let mut messages: Vec<&Message> = Vec::new(); + + // Check Buffer account to see if partial initialization has occurred + let (initial_instructions, balance_needed) = if let Some(account) = rpc_client + .get_account_with_commitment(&buffer_signer.pubkey(), config.commitment)? + .value + { + complete_partial_program_init( + &loader_id, + &config.signers[0].pubkey(), + &buffer_signer.pubkey(), + &account, + UpgradeableLoaderState::buffer_len(data_len)?, + minimum_balance, + true, + )? + } else { + ( + bpf_loader_upgradeable::create_buffer( + &config.signers[0].pubkey(), + &buffer_signer.pubkey(), + minimum_balance, + data_len, + )?, + minimum_balance, + ) + }; + let initial_message = if !initial_instructions.is_empty() { + Some(Message::new( + &initial_instructions, + Some(&config.signers[0].pubkey()), + )) + } else { + None + }; + if let Some(message) = &initial_message { + messages.push(message); + } + + // Create and add write messages + let mut write_messages = vec![]; + for (chunk, i) in program_data.chunks(DATA_CHUNK_SIZE).zip(0..) { + let instruction = bpf_loader_upgradeable::write( + &buffer_signer.pubkey(), + (i * DATA_CHUNK_SIZE) as u32, + chunk.to_vec(), + ); + let message = Message::new(&[instruction], Some(&config.signers[0].pubkey())); + write_messages.push(message); + } + let mut write_message_refs = vec![]; + for message in write_messages.iter() { + write_message_refs.push(message); + } + messages.append(&mut write_message_refs); + + // Create and add final message + let final_message = Message::new( + &[bpf_loader_upgradeable::upgrade( + &program_id, + &buffer_signer.pubkey(), + &upgrade_authority.pubkey(), + &config.signers[0].pubkey(), + )], + Some(&config.signers[0].pubkey()), + ); + messages.push(&final_message); + + check_payer(rpc_client, config, balance_needed, &messages)?; + send_deploy_messages( + rpc_client, + config, + &initial_message, + &write_messages, + &final_message, + buffer_signer, + upgrade_authority, + )?; + + Ok(json!({ + "programId": format!("{}", program_id), + }) + .to_string()) +} + +fn read_and_verify_elf(program_location: &str) -> Result, Box> { + let mut file = File::open(program_location) + .map_err(|err| format!("Unable to open program file: {}", err))?; + let mut program_data = Vec::new(); + file.read_to_end(&mut program_data) + .map_err(|err| format!("Unable to read program file: {}", err))?; + + // Verify the program + Executable::::from_elf( + &program_data, + Some(|x| bpf_verifier::check(x, false)), + Config::default(), + ) + .map_err(|err| format!("ELF error: {}", err))?; + + Ok(program_data) +} + +fn complete_partial_program_init( + loader_id: &Pubkey, + payer_pubkey: &Pubkey, + elf_pubkey: &Pubkey, + account: &Account, + account_data_len: usize, + minimum_balance: u64, + allow_excessive_balance: bool, +) -> Result<(Vec, u64), Box> { + let mut instructions: Vec = vec![]; + let mut balance_needed = 0; + if account.executable { + return Err("Buffer account is already executable".into()); + } + if account.owner != *loader_id && !system_program::check_id(&account.owner) { + return Err("Buffer account is already owned by another account".into()); + } + + if account.data.is_empty() && system_program::check_id(&account.owner) { + instructions.push(system_instruction::allocate( + elf_pubkey, + account_data_len as u64, + )); + if account.owner != *loader_id { + instructions.push(system_instruction::assign(elf_pubkey, &loader_id)); + } + } + if account.lamports < minimum_balance { + let balance = minimum_balance - account.lamports; + instructions.push(system_instruction::transfer( + payer_pubkey, + elf_pubkey, + balance, + )); + balance_needed = balance; + } else if account.lamports > minimum_balance + && system_program::check_id(&account.owner) + && !allow_excessive_balance + { + return Err(format!( + "Buffer account has a balance: {:?}; it may already be in use", + Sol(account.lamports) + ) + .into()); + } + Ok((instructions, balance_needed)) +} + +fn check_payer( + rpc_client: &RpcClient, + config: &CliConfig, + balance_needed: u64, + messages: &[&Message], +) -> Result<(), Box> { + let (_, fee_calculator, _) = rpc_client + .get_recent_blockhash_with_commitment(config.commitment)? + .value; + + // Does the payer have enough? + check_account_for_spend_multiple_fees_with_commitment( + rpc_client, + &config.signers[0].pubkey(), + balance_needed, + &fee_calculator, + messages, + config.commitment, + )?; + Ok(()) +} + +fn send_deploy_messages( + rpc_client: &RpcClient, + config: &CliConfig, + initial_message: &Option, + write_messages: &[Message], + final_message: &Message, + buffer_signer: &dyn Signer, + final_signer: &dyn Signer, +) -> Result<(), Box> { + let payer_signer = config.signers[0]; + if let Some(message) = initial_message { + trace!("Preparing the required accounts"); + + let (blockhash, _, _) = rpc_client + .get_recent_blockhash_with_commitment(config.commitment)? + .value; + + let mut initial_transaction = Transaction::new_unsigned(message.clone()); + // 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 + // extraneous signature. + if message.header.num_required_signatures == 2 { + initial_transaction.try_sign(&[payer_signer, buffer_signer], blockhash)?; + } else { + initial_transaction.try_sign(&[payer_signer], blockhash)?; + } + let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config( + &initial_transaction, + config.commitment, + config.send_transaction_config, + ); + log_instruction_custom_error::(result, &config) + .map_err(|err| format!("Account allocation failed: {}", err))?; + } + + trace!("Writing program data"); + let (blockhash, _, last_valid_slot) = rpc_client + .get_recent_blockhash_with_commitment(config.commitment)? + .value; + let mut write_transactions = vec![]; + for message in write_messages.iter() { + let mut tx = Transaction::new_unsigned(message.clone()); + tx.try_sign(&[payer_signer, buffer_signer], blockhash)?; + write_transactions.push(tx); + } + send_and_confirm_transactions_with_spinner( + &rpc_client, + write_transactions, + &[payer_signer, buffer_signer], + config.commitment, + last_valid_slot, + ) + .map_err(|err| format!("Data writes to account failed: {}", err))?; + + trace!("Deploying program"); + let (blockhash, _, _) = rpc_client + .get_recent_blockhash_with_commitment(config.commitment)? + .value; + + let mut final_tx = Transaction::new_unsigned(final_message.clone()); + final_tx.try_sign(&[payer_signer, final_signer], blockhash)?; + rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &final_tx, + config.commitment, + RpcSendTransactionConfig { + skip_preflight: true, + preflight_commitment: Some(config.commitment.commitment), + ..RpcSendTransactionConfig::default() + }, + ) + .map_err(|e| format!("Deploying program failed: {}", e))?; + Ok(()) +} + +fn create_ephemeral_keypair( +) -> Result<(usize, bip39::Mnemonic, Keypair), Box> { + const WORDS: usize = 12; + let mnemonic = Mnemonic::new(MnemonicType::for_word_count(WORDS)?, Language::English); + let seed = Seed::new(&mnemonic, ""); + let new_keypair = keypair_from_seed(seed.as_bytes())?; + + Ok((WORDS, mnemonic, new_keypair)) +} + +fn report_ephemeral_mnemonic(words: usize, mnemonic: bip39::Mnemonic) { + let phrase: &str = mnemonic.phrase(); + let divider = String::from_utf8(vec![b'='; phrase.len()]).unwrap(); + eprintln!( + "{}\nTo resume a failed deploy, recover the ephemeral keypair file with", + divider + ); + eprintln!( + "`solana-keygen recover` and the following {}-word seed phrase,", + words + ); + eprintln!( + "then pass it as the [BUFFER_SIGNER] argument to `solana upgrade ...`\n{}\n{}\n{}", + divider, phrase, divider + ); +} + +fn send_and_confirm_transactions_with_spinner( + rpc_client: &RpcClient, + mut transactions: Vec, + signer_keys: &T, + commitment: CommitmentConfig, + mut last_valid_slot: Slot, +) -> Result<(), Box> { + let progress_bar = new_spinner_progress_bar(); + let mut send_retries = 5; + let mut leader_schedule: Option = None; + let mut leader_schedule_epoch = 0; + let send_socket = UdpSocket::bind("0.0.0.0:0").unwrap(); + let cluster_nodes = rpc_client.get_cluster_nodes().ok(); + + loop { + let mut status_retries = 15; + + progress_bar.set_message("Finding leader node..."); + let epoch_info = rpc_client.get_epoch_info_with_commitment(commitment)?; + if epoch_info.epoch > leader_schedule_epoch || leader_schedule.is_none() { + leader_schedule = rpc_client + .get_leader_schedule_with_commitment(Some(epoch_info.absolute_slot), commitment)?; + leader_schedule_epoch = epoch_info.epoch; + } + let tpu_address = get_leader_tpu( + min(epoch_info.slot_index + 1, epoch_info.slots_in_epoch), + leader_schedule.as_ref(), + cluster_nodes.as_ref(), + ); + + // Send all transactions + let mut pending_transactions = HashMap::new(); + let num_transactions = transactions.len(); + for transaction in transactions { + if let Some(tpu_address) = tpu_address { + let wire_transaction = + serialize(&transaction).expect("serialization should succeed"); + send_transaction_tpu(&send_socket, &tpu_address, &wire_transaction); + } else { + let _result = rpc_client + .send_transaction_with_config( + &transaction, + RpcSendTransactionConfig { + preflight_commitment: Some(commitment.commitment), + ..RpcSendTransactionConfig::default() + }, + ) + .ok(); + } + pending_transactions.insert(transaction.signatures[0], transaction); + + progress_bar.set_message(&format!( + "[{}/{}] Total Transactions sent", + pending_transactions.len(), + num_transactions + )); + } + + // Collect statuses for all the transactions, drop those that are confirmed + while status_retries > 0 { + status_retries -= 1; + + progress_bar.set_message(&format!( + "[{}/{}] Transactions confirmed", + num_transactions - pending_transactions.len(), + num_transactions + )); + + let mut statuses = vec![]; + let pending_signatures = pending_transactions.keys().cloned().collect::>(); + for pending_signatures_chunk in + pending_signatures.chunks(MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS - 1) + { + statuses.extend( + rpc_client + .get_signature_statuses_with_history(pending_signatures_chunk)? + .value + .into_iter(), + ); + } + assert_eq!(statuses.len(), pending_signatures.len()); + + for (signature, status) in pending_signatures.into_iter().zip(statuses.into_iter()) { + if let Some(status) = status { + if status.confirmations.is_none() || status.confirmations.unwrap() > 1 { + let _ = pending_transactions.remove(&signature); + } + } + progress_bar.set_message(&format!( + "[{}/{}] Transactions confirmed", + num_transactions - pending_transactions.len(), + num_transactions + )); + } + + if pending_transactions.is_empty() { + return Ok(()); + } + + let slot = rpc_client.get_slot_with_commitment(commitment)?; + if slot > last_valid_slot { + break; + } + + if cfg!(not(test)) { + // Retry twice a second + sleep(Duration::from_millis(500)); + } + } + + if send_retries == 0 { + return Err("Transactions failed".into()); + } + send_retries -= 1; + + // Re-sign any failed transactions with a new blockhash and retry + let (blockhash, _fee_calculator, new_last_valid_slot) = rpc_client + .get_recent_blockhash_with_commitment(commitment)? + .value; + last_valid_slot = new_last_valid_slot; + transactions = vec![]; + for (_, mut transaction) in pending_transactions.into_iter() { + transaction.try_sign(signer_keys, blockhash)?; + transactions.push(transaction); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cli::{app, parse_command, process_command}; + use serde_json::Value; + use solana_sdk::signature::write_keypair_file; + + fn make_tmp_path(name: &str) -> String { + let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); + let keypair = Keypair::new(); + + let path = format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey()); + + // whack any possible collision + let _ignored = std::fs::remove_dir_all(&path); + // whack any possible collision + let _ignored = std::fs::remove_file(&path); + + path + } + + #[test] + #[allow(clippy::cognitive_complexity)] + fn test_cli_parse_deploy() { + let test_commands = app("test", "desc", "version"); + + let default_keypair = Keypair::new(); + let keypair_file = make_tmp_path("keypair_file"); + write_keypair_file(&default_keypair, &keypair_file).unwrap(); + let default_signer = DefaultSigner { + path: keypair_file.clone(), + arg_name: "".to_string(), + }; + + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "deploy", + "/Users/test/program.so", + ]); + assert_eq!( + parse_command(&test_deploy, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Deploy { + program_location: "/Users/test/program.so".to_string(), + buffer_signer_index: None, + program_signer_index: None, + program_pubkey: None, + upgrade_authority_signer_index: Some(0), + upgrade_authority_pubkey: Some(default_keypair.pubkey()), + max_len: None, + allow_excessive_balance: false, + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], + } + ); + + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "deploy", + "/Users/test/program.so", + "--max-len", + "42", + ]); + assert_eq!( + parse_command(&test_deploy, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Deploy { + program_location: "/Users/test/program.so".to_string(), + buffer_signer_index: None, + program_signer_index: None, + program_pubkey: None, + upgrade_authority_signer_index: Some(0), + upgrade_authority_pubkey: Some(default_keypair.pubkey()), + max_len: Some(42), + allow_excessive_balance: false, + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], + } + ); + + let buffer_pubkey = Pubkey::new_unique(); + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "deploy", + "/Users/test/program.so", + "--buffer", + &buffer_pubkey.to_string(), + ]); + assert!(parse_command(&test_deploy, &default_signer, &mut None).is_err()); + + let buffer_address = Keypair::new(); + let buffer_address_file = make_tmp_path("buffer_address_file"); + write_keypair_file(&buffer_address, &buffer_address_file).unwrap(); + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "deploy", + "/Users/test/program.so", + "--buffer", + &buffer_address_file, + ]); + assert_eq!( + parse_command(&test_deploy, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Deploy { + program_location: "/Users/test/program.so".to_string(), + buffer_signer_index: Some(1), + program_signer_index: None, + program_pubkey: None, + upgrade_authority_signer_index: Some(0), + upgrade_authority_pubkey: Some(default_keypair.pubkey()), + max_len: None, + allow_excessive_balance: false, + }), + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + read_keypair_file(&buffer_address_file).unwrap().into(), + ], + } + ); + + let program_address = Pubkey::new_unique(); + let test = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "deploy", + "/Users/test/program.so", + "--program-id", + &program_address.to_string(), + ]); + assert_eq!( + parse_command(&test, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Deploy { + program_location: "/Users/test/program.so".to_string(), + buffer_signer_index: None, + program_signer_index: None, + program_pubkey: Some(program_address), + upgrade_authority_signer_index: Some(0), + upgrade_authority_pubkey: Some(default_keypair.pubkey()), + max_len: None, + allow_excessive_balance: false, + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into(),], + } + ); + + let program_address = Keypair::new(); + let program_address_file = make_tmp_path("program_address_file"); + write_keypair_file(&program_address, &program_address_file).unwrap(); + let test = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "deploy", + "/Users/test/program.so", + "--program-id", + &program_address_file, + ]); + assert_eq!( + parse_command(&test, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Deploy { + program_location: "/Users/test/program.so".to_string(), + buffer_signer_index: None, + program_signer_index: Some(1), + program_pubkey: Some(program_address.pubkey()), + upgrade_authority_signer_index: Some(0), + upgrade_authority_pubkey: Some(default_keypair.pubkey()), + max_len: None, + allow_excessive_balance: false, + }), + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + read_keypair_file(&program_address_file).unwrap().into(), + ], + } + ); + + let authority_address = Pubkey::new_unique(); + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "deploy", + "/Users/test/program.so", + "--upgrade-authority", + &authority_address.to_string(), + ]); + assert_eq!( + parse_command(&test_deploy, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Deploy { + program_location: "/Users/test/program.so".to_string(), + buffer_signer_index: None, + program_signer_index: None, + program_pubkey: None, + upgrade_authority_signer_index: None, + upgrade_authority_pubkey: Some(authority_address), + max_len: None, + allow_excessive_balance: false, + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], + } + ); + + let authority_address = Keypair::new(); + let authority_address_file = make_tmp_path("authority_address_file"); + write_keypair_file(&authority_address, &authority_address_file).unwrap(); + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "deploy", + "/Users/test/program.so", + "--upgrade-authority", + &authority_address_file, + ]); + assert_eq!( + parse_command(&test_deploy, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Deploy { + program_location: "/Users/test/program.so".to_string(), + buffer_signer_index: None, + program_signer_index: None, + program_pubkey: None, + upgrade_authority_signer_index: Some(1), + upgrade_authority_pubkey: Some(authority_address.pubkey()), + max_len: None, + allow_excessive_balance: false, + }), + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + read_keypair_file(&authority_address_file).unwrap().into(), + ], + } + ); + + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "deploy", + "/Users/test/program.so", + "--final", + ]); + assert_eq!( + parse_command(&test_deploy, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::Deploy { + program_location: "/Users/test/program.so".to_string(), + buffer_signer_index: None, + program_signer_index: None, + program_pubkey: None, + upgrade_authority_signer_index: None, + upgrade_authority_pubkey: None, + max_len: None, + allow_excessive_balance: false, + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into(),], + } + ); + } + + #[test] + #[allow(clippy::cognitive_complexity)] + fn test_cli_parse_set_upgrade_authority() { + let test_commands = app("test", "desc", "version"); + + let default_keypair = Keypair::new(); + let keypair_file = make_tmp_path("keypair_file"); + write_keypair_file(&default_keypair, &keypair_file).unwrap(); + let default_signer = DefaultSigner { + path: keypair_file.clone(), + arg_name: "".to_string(), + }; + + let program_address = Pubkey::new_unique(); + let new_authority_address = Pubkey::new_unique(); + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "set-upgrade-authority", + &program_address.to_string(), + "--new-upgrade-authority", + &new_authority_address.to_string(), + ]); + assert_eq!( + parse_command(&test_deploy, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::SetUpgradeAuthority { + program: program_address, + upgrade_authority_index: Some(0), + new_upgrade_authority: Some(new_authority_address), + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], + } + ); + + let program_address = Pubkey::new_unique(); + let new_authority_address = Keypair::new(); + let new_authority_address_file = make_tmp_path("authority_address_file"); + write_keypair_file(&new_authority_address, &new_authority_address_file).unwrap(); + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "set-upgrade-authority", + &program_address.to_string(), + "--new-upgrade-authority", + &new_authority_address_file, + ]); + assert_eq!( + parse_command(&test_deploy, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::SetUpgradeAuthority { + program: program_address, + upgrade_authority_index: Some(0), + new_upgrade_authority: Some(new_authority_address.pubkey()), + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into(),], + } + ); + + let program_address = Pubkey::new_unique(); + let new_authority_address = Keypair::new(); + let new_authority_address_file = make_tmp_path("authority_address_file"); + write_keypair_file(&new_authority_address, &new_authority_address_file).unwrap(); + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "set-upgrade-authority", + &program_address.to_string(), + "--final", + ]); + assert_eq!( + parse_command(&test_deploy, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::SetUpgradeAuthority { + program: program_address, + upgrade_authority_index: Some(0), + new_upgrade_authority: None, + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into(),], + } + ); + + let program_address = Pubkey::new_unique(); + let authority_address = Keypair::new(); + let authority_address_file = make_tmp_path("authority_address_file"); + write_keypair_file(&authority_address, &authority_address_file).unwrap(); + let new_authority_address = Keypair::new(); + let new_authority_address_file = make_tmp_path("authority_address_file"); + write_keypair_file(&new_authority_address, &new_authority_address_file).unwrap(); + let test_deploy = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "set-upgrade-authority", + &program_address.to_string(), + "--upgrade-authority", + &authority_address_file, + "--final", + ]); + assert_eq!( + parse_command(&test_deploy, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::SetUpgradeAuthority { + program: program_address, + upgrade_authority_index: Some(1), + new_upgrade_authority: None, + }), + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + read_keypair_file(&authority_address_file).unwrap().into(), + ], + } + ); + } + + #[test] + fn test_cli_keypair_file() { + solana_logger::setup(); + + let default_keypair = Keypair::new(); + let program_address = Keypair::new(); + let deploy_path = make_tmp_path("deploy"); + let mut program_location = PathBuf::from(deploy_path.clone()); + program_location.push("noop"); + program_location.set_extension("so"); + let mut pathbuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + pathbuf.push("tests"); + pathbuf.push("fixtures"); + pathbuf.push("noop"); + pathbuf.set_extension("so"); + let program_keypair_location = program_location.with_file_name("noop-keypair.json"); + std::fs::create_dir_all(deploy_path).unwrap(); + std::fs::copy(pathbuf, program_location.as_os_str()).unwrap(); + write_keypair_file(&program_address, &program_keypair_location).unwrap(); + + let config = CliConfig { + rpc_client: Some(RpcClient::new_mock("".to_string())), + command: CliCommand::Program(ProgramCliCommand::Deploy { + program_location: program_location.to_str().unwrap().to_string(), + buffer_signer_index: None, + program_signer_index: None, + program_pubkey: None, + upgrade_authority_signer_index: None, + upgrade_authority_pubkey: Some(default_keypair.pubkey()), + max_len: None, + allow_excessive_balance: false, + }), + signers: vec![&default_keypair], + ..CliConfig::default() + }; + + let result = process_command(&config); + let json: Value = serde_json::from_str(&result.unwrap()).unwrap(); + let program_id = json + .as_object() + .unwrap() + .get("programId") + .unwrap() + .as_str() + .unwrap(); + + assert_eq!( + program_id.parse::().unwrap(), + program_address.pubkey() + ); + } +} diff --git a/cli/tests/deploy.rs b/cli/tests/program.rs similarity index 66% rename from cli/tests/deploy.rs rename to cli/tests/program.rs index 541c7e2a99..2d174ce75e 100644 --- a/cli/tests/deploy.rs +++ b/cli/tests/program.rs @@ -1,5 +1,8 @@ use serde_json::Value; -use solana_cli::cli::{process_command, CliCommand, CliConfig}; +use solana_cli::{ + cli::{process_command, CliCommand, CliConfig}, + program::ProgramCliCommand, +}; use solana_client::rpc_client::RpcClient; use solana_core::test_validator::TestValidator; use solana_faucet::faucet::run_local_faucet; @@ -13,7 +16,7 @@ use solana_sdk::{ use std::{fs::File, io::Read, path::PathBuf, str::FromStr, sync::mpsc::channel}; #[test] -fn test_cli_deploy_program() { +fn test_cli_program_deploy_non_upgradeable() { solana_logger::setup(); let mut pathbuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -41,25 +44,21 @@ fn test_cli_deploy_program() { 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 { faucet_host: None, faucet_port: faucet_addr.port(), pubkey: None, lamports: 4 * minimum_balance_for_rent_exemption, // min balance for rent exemption for three programs + leftover for tx processing }; - config.signers = vec![&keypair]; process_command(&config).unwrap(); - config.command = CliCommand::ProgramDeploy { + config.command = CliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - buffer: None, + address: None, use_deprecated_loader: false, - use_upgradeable_loader: false, allow_excessive_balance: false, - upgrade_authority: None, - max_len: None, }; - let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); let program_id_str = json @@ -78,24 +77,19 @@ fn test_cli_deploy_program() { assert_eq!(account0.lamports, minimum_balance_for_rent_exemption); assert_eq!(account0.owner, bpf_loader::id()); assert_eq!(account0.executable, true); - let mut file = File::open(pathbuf.to_str().unwrap().to_string()).unwrap(); let mut elf = Vec::new(); file.read_to_end(&mut elf).unwrap(); - assert_eq!(account0.data, elf); // Test custom address let custom_address_keypair = Keypair::new(); config.signers = vec![&keypair, &custom_address_keypair]; - config.command = CliCommand::ProgramDeploy { + config.command = CliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - buffer: Some(1), + address: Some(1), use_deprecated_loader: false, - use_upgradeable_loader: false, allow_excessive_balance: false, - upgrade_authority: None, - max_len: None, }; process_command(&config).unwrap(); let account1 = rpc_client @@ -106,43 +100,36 @@ fn test_cli_deploy_program() { assert_eq!(account1.lamports, minimum_balance_for_rent_exemption); assert_eq!(account1.owner, bpf_loader::id()); assert_eq!(account1.executable, true); - assert_eq!(account0.data, account1.data); + assert_eq!(account1.data, account0.data); // Attempt to redeploy to the same address process_command(&config).unwrap_err(); // Attempt to deploy to account with excess balance let custom_address_keypair = Keypair::new(); + config.signers = vec![&custom_address_keypair]; config.command = CliCommand::Airdrop { faucet_host: None, faucet_port: faucet_addr.port(), pubkey: None, lamports: 2 * minimum_balance_for_rent_exemption, // Anything over minimum_balance_for_rent_exemption should trigger err }; - config.signers = vec![&custom_address_keypair]; process_command(&config).unwrap(); - config.signers = vec![&keypair, &custom_address_keypair]; - config.command = CliCommand::ProgramDeploy { + config.command = CliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - buffer: Some(1), + address: Some(1), use_deprecated_loader: false, - use_upgradeable_loader: false, allow_excessive_balance: false, - upgrade_authority: None, - max_len: None, }; process_command(&config).unwrap_err(); // Use forcing parameter to deploy to account with excess balance - config.command = CliCommand::ProgramDeploy { + config.command = CliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - buffer: Some(1), + address: Some(1), use_deprecated_loader: false, - use_upgradeable_loader: false, allow_excessive_balance: true, - upgrade_authority: None, - max_len: None, }; process_command(&config).unwrap(); let account2 = rpc_client @@ -153,11 +140,11 @@ fn test_cli_deploy_program() { assert_eq!(account2.lamports, 2 * minimum_balance_for_rent_exemption); assert_eq!(account2.owner, bpf_loader::id()); assert_eq!(account2.executable, true); - assert_eq!(account0.data, account2.data); + assert_eq!(account2.data, account0.data); } #[test] -fn test_cli_deploy_upgradeable_program() { +fn test_cli_program_deploy_no_authority() { solana_logger::setup(); let mut pathbuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -179,11 +166,6 @@ fn test_cli_deploy_upgradeable_program() { let mut program_data = Vec::new(); file.read_to_end(&mut program_data).unwrap(); let max_len = program_data.len(); - println!( - "max_len {:?} {:?}", - max_len, - UpgradeableLoaderState::programdata_len(max_len) - ); let minimum_balance_for_programdata = rpc_client .get_minimum_balance_for_rent_exemption( UpgradeableLoaderState::programdata_len(max_len).unwrap(), @@ -206,16 +188,18 @@ fn test_cli_deploy_upgradeable_program() { config.signers = vec![&keypair]; process_command(&config).unwrap(); - // Deploy and attempt to upgrade a non-upgradeable program - config.command = CliCommand::ProgramDeploy { + // Deploy a program with no authority + config.signers = vec![&keypair]; + config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - buffer: None, - use_deprecated_loader: false, - use_upgradeable_loader: true, + program_signer_index: None, + program_pubkey: None, + buffer_signer_index: None, allow_excessive_balance: false, - upgrade_authority: None, - max_len: Some(max_len), - }; + upgrade_authority_signer_index: None, + upgrade_authority_pubkey: None, + max_len: None, + }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); let program_id_str = json @@ -227,25 +211,132 @@ fn test_cli_deploy_upgradeable_program() { .unwrap(); let program_id = Pubkey::from_str(&program_id_str).unwrap(); + // Attempt to upgrade the program config.signers = vec![&keypair, &upgrade_authority]; - config.command = CliCommand::ProgramUpgrade { + config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - program: program_id, - buffer: None, - upgrade_authority: 1, - }; + program_signer_index: None, + program_pubkey: Some(program_id), + buffer_signer_index: None, + allow_excessive_balance: false, + upgrade_authority_signer_index: Some(1), + upgrade_authority_pubkey: Some(upgrade_authority.pubkey()), + max_len: None, + }); process_command(&config).unwrap_err(); +} + +#[test] +fn test_cli_program_deploy_with_authority() { + solana_logger::setup(); + + let mut pathbuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + pathbuf.push("tests"); + pathbuf.push("fixtures"); + pathbuf.push("noop"); + pathbuf.set_extension("so"); + + let mint_keypair = Keypair::new(); + let test_validator = TestValidator::with_no_fees(mint_keypair.pubkey()); + + let (sender, receiver) = channel(); + run_local_faucet(mint_keypair, sender, None); + let faucet_addr = receiver.recv().unwrap(); + + let rpc_client = RpcClient::new(test_validator.rpc_url()); + + let mut file = File::open(pathbuf.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::programdata_len(max_len).unwrap(), + ) + .unwrap(); + let minimum_balance_for_program = rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::program_len().unwrap()) + .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 { + faucet_host: None, + faucet_port: faucet_addr.port(), + 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: pathbuf.to_str().unwrap().to_string(), + program_signer_index: Some(2), + program_pubkey: Some(program_keypair.pubkey()), + buffer_signer_index: None, + allow_excessive_balance: false, + upgrade_authority_signer_index: Some(1), + upgrade_authority_pubkey: Some(upgrade_authority.pubkey()), + max_len: Some(max_len), + }); + let response = process_command(&config); + let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); + let program_id_str = json + .as_object() + .unwrap() + .get("programId") + .unwrap() + .as_str() + .unwrap(); + assert_eq!( + program_keypair.pubkey(), + Pubkey::from_str(&program_id_str).unwrap() + ); + let program_account = rpc_client + .get_account_with_commitment(&program_keypair.pubkey(), CommitmentConfig::recent()) + .unwrap() + .value + .unwrap(); + assert_eq!(program_account.lamports, minimum_balance_for_program); + assert_eq!(program_account.owner, bpf_loader_upgradeable::id()); + assert_eq!(program_account.executable, true); + let (programdata_pubkey, _) = Pubkey::find_program_address( + &[program_keypair.pubkey().as_ref()], + &bpf_loader_upgradeable::id(), + ); + let programdata_account = rpc_client + .get_account_with_commitment(&programdata_pubkey, CommitmentConfig::recent()) + .unwrap() + .value + .unwrap(); + assert_eq!( + programdata_account.lamports, + minimum_balance_for_programdata + ); + assert_eq!(programdata_account.owner, bpf_loader_upgradeable::id()); + assert_eq!(programdata_account.executable, false); + assert_eq!( + programdata_account.data[UpgradeableLoaderState::programdata_data_offset().unwrap()..], + program_data[..] + ); // Deploy the upgradeable program - config.command = CliCommand::ProgramDeploy { + config.signers = vec![&keypair, &upgrade_authority]; + config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - buffer: None, - use_deprecated_loader: false, - use_upgradeable_loader: true, + program_signer_index: None, + program_pubkey: None, + buffer_signer_index: None, allow_excessive_balance: false, - upgrade_authority: Some(upgrade_authority.pubkey()), + upgrade_authority_signer_index: Some(1), + upgrade_authority_pubkey: Some(upgrade_authority.pubkey()), max_len: Some(max_len), - }; + }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); let program_id_str = json @@ -284,12 +375,16 @@ fn test_cli_deploy_upgradeable_program() { // Upgrade the program config.signers = vec![&keypair, &upgrade_authority]; - config.command = CliCommand::ProgramUpgrade { + config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - program: program_id, - buffer: None, - upgrade_authority: 1, - }; + program_signer_index: None, + program_pubkey: Some(program_id), + buffer_signer_index: None, + allow_excessive_balance: false, + upgrade_authority_signer_index: Some(1), + upgrade_authority_pubkey: Some(upgrade_authority.pubkey()), + max_len: Some(max_len), + }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); let program_id_str = json @@ -329,11 +424,11 @@ fn test_cli_deploy_upgradeable_program() { // Set a new authority let new_upgrade_authority = Keypair::new(); config.signers = vec![&keypair, &upgrade_authority]; - config.command = CliCommand::SetProgramUpgradeAuthority { + config.command = CliCommand::Program(ProgramCliCommand::SetUpgradeAuthority { program: program_id, - upgrade_authority: 1, + upgrade_authority_index: Some(1), new_upgrade_authority: Some(new_upgrade_authority.pubkey()), - }; + }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); let new_upgrade_authority_str = json @@ -350,12 +445,16 @@ fn test_cli_deploy_upgradeable_program() { // Upgrade with new authority config.signers = vec![&keypair, &new_upgrade_authority]; - config.command = CliCommand::ProgramUpgrade { + config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - program: program_id, - buffer: None, - upgrade_authority: 1, - }; + program_signer_index: None, + program_pubkey: Some(program_id), + buffer_signer_index: None, + allow_excessive_balance: false, + upgrade_authority_signer_index: Some(1), + upgrade_authority_pubkey: Some(new_upgrade_authority.pubkey()), + max_len: None, + }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); let program_id_str = json @@ -392,13 +491,13 @@ fn test_cli_deploy_upgradeable_program() { program_data[..] ); - // Set a no authority + // Set no authority config.signers = vec![&keypair, &new_upgrade_authority]; - config.command = CliCommand::SetProgramUpgradeAuthority { + config.command = CliCommand::Program(ProgramCliCommand::SetUpgradeAuthority { program: program_id, - upgrade_authority: 1, + upgrade_authority_index: Some(1), new_upgrade_authority: None, - }; + }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); let new_upgrade_authority_str = json @@ -412,11 +511,15 @@ fn test_cli_deploy_upgradeable_program() { // Upgrade with no authority config.signers = vec![&keypair, &new_upgrade_authority]; - config.command = CliCommand::ProgramUpgrade { + config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: pathbuf.to_str().unwrap().to_string(), - program: program_id, - buffer: None, - upgrade_authority: 1, - }; + program_signer_index: None, + program_pubkey: Some(program_id), + buffer_signer_index: None, + allow_excessive_balance: false, + upgrade_authority_signer_index: Some(1), + upgrade_authority_pubkey: Some(new_upgrade_authority.pubkey()), + max_len: None, + }); process_command(&config).unwrap_err(); } diff --git a/client/src/mock_sender.rs b/client/src/mock_sender.rs index 7e690c74c7..73cd654f65 100644 --- a/client/src/mock_sender.rs +++ b/client/src/mock_sender.rs @@ -48,6 +48,10 @@ impl RpcSender for MockSender { return Ok(Value::Null); } let val = match request { + RpcRequest::GetAccountInfo => serde_json::to_value(Response { + context: RpcResponseContext { slot: 1 }, + value: Value::Null, + })?, RpcRequest::GetBalance => serde_json::to_value(Response { context: RpcResponseContext { slot: 1 }, value: Value::Number(Number::from(50)), diff --git a/sdk/cargo-build-bpf/src/main.rs b/sdk/cargo-build-bpf/src/main.rs index f148401773..2b220035bf 100644 --- a/sdk/cargo-build-bpf/src/main.rs +++ b/sdk/cargo-build-bpf/src/main.rs @@ -217,7 +217,7 @@ fn build_bpf_package( println!(); println!("To deploy this program:"); - println!(" $ solana deploy {}", program_so.display()); + println!(" $ solana program deploy {}", program_so.display()); } else if config.dump { println!("Note: --dump is only available for crates with a cdylib target"); }