Skip to content

Commit

Permalink
Adds CLI command.
Browse files Browse the repository at this point in the history
  • Loading branch information
Lichtso committed Feb 6, 2025
1 parent 68c8f8f commit 7d0f4d0
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 1 deletion.
19 changes: 19 additions & 0 deletions cli-output/src/cli_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2468,6 +2468,25 @@ impl fmt::Display for CliUpgradeableProgramExtended {
}
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliUpgradeableProgramMigrated {
pub program_id: String,
}
impl QuietDisplay for CliUpgradeableProgramMigrated {}
impl VerboseDisplay for CliUpgradeableProgramMigrated {}
impl fmt::Display for CliUpgradeableProgramMigrated {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f)?;
writeln!(
f,
"Migrated Program Id {} from loader-v3 to loader-v4",
&self.program_id,
)?;
Ok(())
}
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliUpgradeableBuffer {
Expand Down
205 changes: 204 additions & 1 deletion cli/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use {
return_signers_with_config, CliProgram, CliProgramAccountType, CliProgramAuthority,
CliProgramBuffer, CliProgramId, CliUpgradeableBuffer, CliUpgradeableBuffers,
CliUpgradeableProgram, CliUpgradeableProgramClosed, CliUpgradeableProgramExtended,
CliUpgradeablePrograms, ReturnSignersConfig,
CliUpgradeableProgramMigrated, CliUpgradeablePrograms, ReturnSignersConfig,
},
solana_client::{
connection_cache::ConnectionCache,
Expand Down Expand Up @@ -171,6 +171,11 @@ pub enum ProgramCliCommand {
program_pubkey: Pubkey,
additional_bytes: u32,
},
MigrateProgram {
program_pubkey: Pubkey,
authority_signer_index: SignerIndex,
compute_unit_price: Option<u64>,
},
}

pub trait ProgramSubCommands {
Expand Down Expand Up @@ -632,6 +637,33 @@ impl ProgramSubCommands for App<'_, '_> {
data account",
),
),
)
.subcommand(
SubCommand::with_name("migrate")
.about(
"Migrates an upgradeable program to loader-v4",
)
.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("authority")
.long("authority")
.value_name("AUTHORITY_SIGNER")
.takes_value(true)
.validator(is_valid_signer)
.help(
"Upgrade authority [default: the default configured \
keypair]",
),
)
.arg(compute_unit_price_arg()),
),
)
.subcommand(
Expand Down Expand Up @@ -991,6 +1023,32 @@ pub fn parse_program_subcommand(
signers: signer_info.signers,
}
}
("migrate", Some(matches)) => {
let program_pubkey = pubkey_of(matches, "program_id").unwrap();

let (authority_signer, authority_pubkey) =
signer_of(matches, "authority", wallet_manager)?;

let signer_info = default_signer.generate_unique_signers(
vec![
Some(default_signer.signer_from_path(matches, wallet_manager)?),
authority_signer,
],
matches,
wallet_manager,
)?;

let compute_unit_price = value_of(matches, "compute_unit_price");

CliCommandInfo {
command: CliCommand::Program(ProgramCliCommand::MigrateProgram {
program_pubkey,
authority_signer_index: signer_info.index_of(authority_pubkey).unwrap(),
compute_unit_price,
}),
signers: signer_info.signers,
}
}
_ => unreachable!(),
};
Ok(response)
Expand Down Expand Up @@ -1175,6 +1233,17 @@ pub fn process_program_subcommand(
program_pubkey,
additional_bytes,
} => process_extend_program(&rpc_client, config, *program_pubkey, *additional_bytes),
ProgramCliCommand::MigrateProgram {
program_pubkey,
authority_signer_index,
compute_unit_price,
} => process_migrate_program(
&rpc_client,
config,
*program_pubkey,
*authority_signer_index,
*compute_unit_price,
),
}
}

Expand Down Expand Up @@ -2387,6 +2456,100 @@ fn process_extend_program(
}))
}

fn process_migrate_program(
rpc_client: &RpcClient,
config: &CliConfig,
program_pubkey: Pubkey,
authority_signer_index: SignerIndex,
compute_unit_price: Option<u64>,
) -> ProcessResult {
let payer_pubkey = config.signers[0].pubkey();
let authority_signer = config.signers[authority_signer_index];

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 Ok(UpgradeableLoaderState::Program {
programdata_address: programdata_pubkey,
}) = program_account.state()
else {
return Err(format!("Account {program_pubkey} is not an upgradeable program").into());
};

let Some(programdata_account) = rpc_client
.get_account_with_commitment(&programdata_pubkey, config.commitment)?
.value
else {
return Err(format!("Program {program_pubkey} is closed").into());
};

let upgrade_authority_address = match programdata_account.state() {
Ok(UpgradeableLoaderState::ProgramData {
slot: _slot,
upgrade_authority_address,
}) => upgrade_authority_address,
_ => None,
};

if authority_signer.pubkey() != upgrade_authority_address.unwrap_or(program_pubkey) {
return Err(format!(
"Upgrade authority {:?} does not match {:?}",
upgrade_authority_address,
Some(authority_signer.pubkey())
)
.into());
}

let blockhash = rpc_client.get_latest_blockhash()?;
let mut message = Message::new(
&vec![bpf_loader_upgradeable::migrate_program(
&programdata_pubkey,
&program_pubkey,
&authority_signer.pubkey(),
)]
.with_compute_unit_config(&ComputeUnitConfig {
compute_unit_price,
compute_unit_limit: ComputeUnitLimit::Simulated,
}),
Some(&payer_pubkey),
);
simulate_and_update_compute_unit_limit(&ComputeUnitLimit::Simulated, rpc_client, &mut message)?;

let mut tx = Transaction::new_unsigned(message);
tx.try_sign(&[config.signers[0], config.signers[1]], blockhash)?;
let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
&tx,
config.commitment,
config.send_transaction_config,
);
if let Err(err) = result {
if let ClientErrorKind::TransactionError(TransactionError::InstructionError(
_,
InstructionError::InvalidInstructionData,
)) = err.kind()
{
return Err("Migrating a program is not supported by the cluster".into());
} else {
return Err(format!("Migrate program failed: {err}").into());
}
}

Ok(config
.output_format
.formatted_string(&CliUpgradeableProgramMigrated {
program_id: program_pubkey.to_string(),
}))
}

pub fn calculate_max_chunk_size<F>(create_msg: &F) -> usize
where
F: Fn(u32, Vec<u8>) -> Message,
Expand Down Expand Up @@ -4255,6 +4418,46 @@ mod tests {
);
}

#[test]
fn test_cli_parse_migrate_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);

let program_pubkey = Pubkey::new_unique();
let authority_keypair = Keypair::new();
let authority_keypair_file = make_tmp_path("authority_keypair_file");
write_keypair_file(&authority_keypair, &authority_keypair_file).unwrap();

let test_command = test_commands.clone().get_matches_from(vec![
"test",
"program",
"migrate",
&program_pubkey.to_string(),
"--authority",
&authority_keypair_file.to_string(),
"--with-compute-unit-price",
"1",
]);
assert_eq!(
parse_command(&test_command, &default_signer, &mut None).unwrap(),
CliCommandInfo {
command: CliCommand::Program(ProgramCliCommand::MigrateProgram {
program_pubkey,
authority_signer_index: 1,
compute_unit_price: Some(1),
}),
signers: vec![
Box::new(read_keypair_file(&keypair_file).unwrap()),
Box::new(read_keypair_file(&authority_keypair_file).unwrap()),
],
}
);
}

#[test]
fn test_cli_keypair_file() {
solana_logger::setup();
Expand Down
79 changes: 79 additions & 0 deletions cli/tests/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,85 @@ fn test_cli_program_extend_program() {
process_command(&config).unwrap();
}

#[test]
fn test_cli_program_migrate_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 test_validator = test_validator_genesis(mint_keypair)
.start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified)
.expect("validator start failed");

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()),
fee_payer_signer_index: 0,
program_signer_index: Some(2),
program_pubkey: Some(program_keypair.pubkey()),
buffer_signer_index: None,
buffer_pubkey: None,
upgrade_authority_signer_index: 1,
is_final: false,
max_len: Some(max_len),
skip_fee_check: false,
compute_unit_price: None,
max_sign_attempts: 5,
auto_extend: true,
use_rpc: false,
skip_feature_verification: true,
});
config.output_format = OutputFormat::JsonCompact;
process_command(&config).unwrap();

// Wait one slot to avoid "Program was deployed in this block already" error
wait_n_slots(&rpc_client, 1);

// Migrate program
config.signers = vec![&keypair, &upgrade_authority];
config.command = CliCommand::Program(ProgramCliCommand::MigrateProgram {
program_pubkey: program_keypair.pubkey(),
authority_signer_index: 1,
compute_unit_price: Some(1),
});
process_command(&config).unwrap();
}

#[test]
fn test_cli_program_write_buffer() {
solana_logger::setup();
Expand Down

0 comments on commit 7d0f4d0

Please sign in to comment.