cli: add solana program extend subcommand (#34043)

This commit is contained in:
Justin Starry 2023-11-16 09:52:41 +08:00 committed by GitHub
parent b7f839ea18
commit 34f5c68416
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 281 additions and 1 deletions

View File

@ -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 {

View File

@ -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::<u32>)
.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<F>(create_msg: &F) -> usize
where
F: Fn(u32, Vec<u8>) -> 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();

View File

@ -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();