From 8889a6c0100e0c9e5eb4f95347573b16e52bb379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Mei=C3=9Fner?= Date: Mon, 3 Feb 2025 16:19:06 +0000 Subject: [PATCH] Adds CLI command. --- cli-output/src/cli_output.rs | 19 ++++ cli/src/program.rs | 205 ++++++++++++++++++++++++++++++++++- cli/tests/program.rs | 79 ++++++++++++++ 3 files changed, 302 insertions(+), 1 deletion(-) diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 533ca9dd1129f4..a54baa18aaa521 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -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 { diff --git a/cli/src/program.rs b/cli/src/program.rs index eb6ccde61cc0cd..6c1a399caac52e 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -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, @@ -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, + }, } pub trait ProgramSubCommands { @@ -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( @@ -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) @@ -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, + ), } } @@ -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, +) -> 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(create_msg: &F) -> usize where F: Fn(u32, Vec) -> Message, @@ -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(); diff --git a/cli/tests/program.rs b/cli/tests/program.rs index 52041a7c403147..39503526feec34 100644 --- a/cli/tests/program.rs +++ b/cli/tests/program.rs @@ -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();