diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 8fe188ad7c..7e51a05786 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -2316,6 +2316,26 @@ impl fmt::Display for CliUpgradeableProgramClosed { } } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliUpgradeableProgramExtended { + pub program_id: String, + pub additional_bytes: u32, +} +impl QuietDisplay for CliUpgradeableProgramExtended {} +impl VerboseDisplay for CliUpgradeableProgramExtended {} +impl fmt::Display for CliUpgradeableProgramExtended { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + writeln!( + f, + "Extended Program Id {} by {} bytes", + &self.program_id, self.additional_bytes, + )?; + Ok(()) + } +} + #[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliUpgradeableBuffer { diff --git a/cli/src/program.rs b/cli/src/program.rs index 04f1a4b3f7..7a37a0a93d 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -17,7 +17,7 @@ use { solana_cli_output::{ CliProgram, CliProgramAccountType, CliProgramAuthority, CliProgramBuffer, CliProgramId, CliUpgradeableBuffer, CliUpgradeableBuffers, CliUpgradeableProgram, - CliUpgradeableProgramClosed, CliUpgradeablePrograms, + CliUpgradeableProgramClosed, CliUpgradeableProgramExtended, CliUpgradeablePrograms, }, solana_client::{ connection_cache::ConnectionCache, @@ -124,6 +124,10 @@ pub enum ProgramCliCommand { use_lamports_unit: bool, bypass_warning: bool, }, + ExtendProgram { + program_pubkey: Pubkey, + additional_bytes: u32, + }, } pub trait ProgramSubCommands { @@ -417,6 +421,28 @@ impl ProgramSubCommands for App<'_, '_> { .help("Bypass the permanent program closure warning"), ), ) + .subcommand( + SubCommand::with_name("extend") + .about("Extend the length of an upgradeable program to deploy larger programs") + .arg( + Arg::with_name("program_id") + .index(1) + .value_name("PROGRAM_ID") + .takes_value(true) + .required(true) + .validator(is_valid_pubkey) + .help("Address of the program to extend"), + ) + .arg( + Arg::with_name("additional_bytes") + .index(2) + .value_name("ADDITIONAL_BYTES") + .takes_value(true) + .required(true) + .validator(is_parsable::) + .help("Number of bytes that will be allocated for the program's data account") + ) + ) ) .subcommand( SubCommand::with_name("deploy") @@ -675,6 +701,26 @@ pub fn parse_program_subcommand( signers: signer_info.signers, } } + ("extend", Some(matches)) => { + let program_pubkey = pubkey_of(matches, "program_id").unwrap(); + let additional_bytes = value_of(matches, "additional_bytes").unwrap(); + + let signer_info = default_signer.generate_unique_signers( + vec![Some( + default_signer.signer_from_path(matches, wallet_manager)?, + )], + matches, + wallet_manager, + )?; + + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::ExtendProgram { + program_pubkey, + additional_bytes, + }), + signers: signer_info.signers, + } + } _ => unreachable!(), }; Ok(response) @@ -799,6 +845,10 @@ pub fn process_program_subcommand( *use_lamports_unit, *bypass_warning, ), + ProgramCliCommand::ExtendProgram { + program_pubkey, + additional_bytes, + } => process_extend_program(&rpc_client, config, *program_pubkey, *additional_bytes), } } @@ -1716,6 +1766,100 @@ fn process_close( } } +fn process_extend_program( + rpc_client: &RpcClient, + config: &CliConfig, + program_pubkey: Pubkey, + additional_bytes: u32, +) -> ProcessResult { + let payer_pubkey = config.signers[0].pubkey(); + + if additional_bytes == 0 { + return Err("Additional bytes must be greater than zero".into()); + } + + let program_account = match rpc_client + .get_account_with_commitment(&program_pubkey, config.commitment)? + .value + { + Some(program_account) => Ok(program_account), + None => Err(format!("Unable to find program {program_pubkey}")), + }?; + + if !bpf_loader_upgradeable::check_id(&program_account.owner) { + return Err(format!("Account {program_pubkey} is not an upgradeable program").into()); + } + + let programdata_pubkey = match program_account.state() { + Ok(UpgradeableLoaderState::Program { + programdata_address: programdata_pubkey, + }) => Ok(programdata_pubkey), + _ => Err(format!( + "Account {program_pubkey} is not an upgradeable program" + )), + }?; + + let programdata_account = match rpc_client + .get_account_with_commitment(&programdata_pubkey, config.commitment)? + .value + { + Some(programdata_account) => Ok(programdata_account), + None => Err(format!("Program {program_pubkey} is closed")), + }?; + + let upgrade_authority_address = match programdata_account.state() { + Ok(UpgradeableLoaderState::ProgramData { + slot: _, + upgrade_authority_address, + }) => Ok(upgrade_authority_address), + _ => Err(format!("Program {program_pubkey} is closed")), + }?; + + match upgrade_authority_address { + None => Err(format!("Program {program_pubkey} is not upgradeable")), + _ => Ok(()), + }?; + + let blockhash = rpc_client.get_latest_blockhash()?; + + let mut tx = Transaction::new_unsigned(Message::new( + &[bpf_loader_upgradeable::extend_program( + &program_pubkey, + Some(&payer_pubkey), + additional_bytes, + )], + Some(&payer_pubkey), + )); + + tx.try_sign(&[config.signers[0]], blockhash)?; + let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config( + &tx, + config.commitment, + RpcSendTransactionConfig { + preflight_commitment: Some(config.commitment.commitment), + ..RpcSendTransactionConfig::default() + }, + ); + if let Err(err) = result { + if let ClientErrorKind::TransactionError(TransactionError::InstructionError( + _, + InstructionError::InvalidInstructionData, + )) = err.kind() + { + return Err("Extending a program is not supported by the cluster".into()); + } else { + return Err(format!("Extend program failed: {err}").into()); + } + } + + Ok(config + .output_format + .formatted_string(&CliUpgradeableProgramExtended { + program_id: program_pubkey.to_string(), + additional_bytes, + })) +} + pub fn calculate_max_chunk_size(create_msg: &F) -> usize where F: Fn(u32, Vec) -> Message, @@ -3115,6 +3259,38 @@ mod tests { ); } + #[test] + fn test_cli_parse_extend_program() { + let test_commands = get_clap_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::new("", &keypair_file); + + // defaults + let program_pubkey = Pubkey::new_unique(); + let additional_bytes = 100; + + let test_command = test_commands.clone().get_matches_from(vec![ + "test", + "program", + "extend", + &program_pubkey.to_string(), + &additional_bytes.to_string(), + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::Program(ProgramCliCommand::ExtendProgram { + program_pubkey, + additional_bytes + }), + signers: vec![read_keypair_file(&keypair_file).unwrap().into()], + } + ); + } + #[test] fn test_cli_keypair_file() { solana_logger::setup(); diff --git a/cli/tests/program.rs b/cli/tests/program.rs index de5ef8cd01..5bd10a92b7 100644 --- a/cli/tests/program.rs +++ b/cli/tests/program.rs @@ -726,6 +726,90 @@ fn test_cli_program_close_program() { assert_eq!(programdata_lamports, recipient_account.lamports); } +#[test] +fn test_cli_program_extend_program() { + solana_logger::setup(); + + let mut noop_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + noop_path.push("tests"); + noop_path.push("fixtures"); + noop_path.push("noop"); + noop_path.set_extension("so"); + + let mint_keypair = Keypair::new(); + let mint_pubkey = mint_keypair.pubkey(); + let faucet_addr = run_local_faucet(mint_keypair, None); + let test_validator = + TestValidator::with_no_fees(mint_pubkey, Some(faucet_addr), SocketAddrSpace::Unspecified); + + let rpc_client = + RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed()); + + let mut file = File::open(noop_path.to_str().unwrap()).unwrap(); + let mut program_data = Vec::new(); + file.read_to_end(&mut program_data).unwrap(); + let max_len = program_data.len(); + let minimum_balance_for_programdata = rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_programdata( + max_len, + )) + .unwrap(); + let minimum_balance_for_program = rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()) + .unwrap(); + let upgrade_authority = Keypair::new(); + + let mut config = CliConfig::recent_for_tests(); + let keypair = Keypair::new(); + config.json_rpc_url = test_validator.rpc_url(); + config.signers = vec![&keypair]; + config.command = CliCommand::Airdrop { + pubkey: None, + lamports: 100 * minimum_balance_for_programdata + minimum_balance_for_program, + }; + process_command(&config).unwrap(); + + // Deploy the upgradeable program + let program_keypair = Keypair::new(); + config.signers = vec![&keypair, &upgrade_authority, &program_keypair]; + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: Some(noop_path.to_str().unwrap().to_string()), + program_signer_index: Some(2), + program_pubkey: Some(program_keypair.pubkey()), + buffer_signer_index: None, + buffer_pubkey: None, + allow_excessive_balance: false, + upgrade_authority_signer_index: 1, + is_final: false, + max_len: Some(max_len), + skip_fee_check: false, + }); + config.output_format = OutputFormat::JsonCompact; + process_command(&config).unwrap(); + + let (programdata_pubkey, _) = Pubkey::find_program_address( + &[program_keypair.pubkey().as_ref()], + &bpf_loader_upgradeable::id(), + ); + + // Wait one slot to avoid "Program was deployed in this block already" error + wait_n_slots(&rpc_client, 1); + + // Extend program + let additional_bytes = 100; + config.signers = vec![&keypair]; + config.command = CliCommand::Program(ProgramCliCommand::ExtendProgram { + program_pubkey: program_keypair.pubkey(), + additional_bytes, + }); + process_command(&config).unwrap(); + + let programdata_account = rpc_client.get_account(&programdata_pubkey).unwrap(); + let expected_len = + UpgradeableLoaderState::size_of_programdata(max_len + additional_bytes as usize); + assert_eq!(expected_len, programdata_account.data.len()); +} + #[test] fn test_cli_program_write_buffer() { solana_logger::setup();