Skip to content

Commit

Permalink
cli: add solana program extend subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
jstarry committed Nov 13, 2023
1 parent ae30572 commit 32823e1
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 1 deletion.
20 changes: 20 additions & 0 deletions cli-output/src/cli_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
142 changes: 141 additions & 1 deletion cli/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -124,6 +124,10 @@ pub enum ProgramCliCommand {
use_lamports_unit: bool,
bypass_warning: bool,
},
ExtendProgram {
program_pubkey: Pubkey,
additional_bytes: u32,
},
}

pub trait ProgramSubCommands {
Expand Down Expand Up @@ -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::<u32>)
.help("Number of bytes that will be allocated for the program's data account")
)
)
)
.subcommand(
SubCommand::with_name("deploy")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -1716,6 +1766,96 @@ 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();

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.into(),
)],
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: additional_bytes.into(),
}))
}

pub fn calculate_max_chunk_size<F>(create_msg: &F) -> usize
where
F: Fn(u32, Vec<u8>) -> Message,
Expand Down
84 changes: 84 additions & 0 deletions cli/tests/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 32823e1

Please sign in to comment.