From 62888d297a4e7027aa0bf218d921287143922dee Mon Sep 17 00:00:00 2001 From: Pankaj Garg Date: Tue, 19 Sep 2023 08:27:35 -0700 Subject: [PATCH] Implement CLI front-end for loader-v4 commands (#33289) * Implement CLI front-end for loader-v4 commands * remove dead_code clippy overrides * Add unit tests --- cli/src/clap_app.rs | 4 +- cli/src/cli.rs | 11 +- cli/src/program_v4.rs | 605 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 607 insertions(+), 13 deletions(-) diff --git a/cli/src/clap_app.rs b/cli/src/clap_app.rs index 0c4b14f97..74d9b998b 100644 --- a/cli/src/clap_app.rs +++ b/cli/src/clap_app.rs @@ -1,7 +1,8 @@ use { crate::{ address_lookup_table::AddressLookupTableSubCommands, cli::*, cluster_query::*, feature::*, - inflation::*, nonce::*, program::*, stake::*, validator_info::*, vote::*, wallet::*, + inflation::*, nonce::*, program::*, program_v4::ProgramV4SubCommands, stake::*, + validator_info::*, vote::*, wallet::*, }, clap::{App, AppSettings, Arg, ArgGroup, SubCommand}, solana_clap_utils::{self, hidden_unless_forced, input_validators::*, keypair::*}, @@ -143,6 +144,7 @@ pub fn get_clap_app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> A .inflation_subcommands() .nonce_subcommands() .program_subcommands() + .program_v4_subcommands() .address_lookup_table_subcommands() .stake_subcommands() .validator_info_subcommands() diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 9db77dbc7..e6960c3fa 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,7 +1,7 @@ use { crate::{ address_lookup_table::*, clap_app::*, cluster_query::*, feature::*, inflation::*, nonce::*, - program::*, spend_utils::*, stake::*, validator_info::*, vote::*, wallet::*, + program::*, program_v4::*, spend_utils::*, stake::*, validator_info::*, vote::*, wallet::*, }, clap::{crate_description, crate_name, value_t_or_exit, ArgMatches, Shell}, log::*, @@ -175,6 +175,7 @@ pub enum CliCommand { // Program Deployment Deploy, Program(ProgramCliCommand), + ProgramV4(ProgramV4CliCommand), // Stake Commands CreateStakeAccount { stake_account: SignerIndex, @@ -687,6 +688,9 @@ pub fn parse_command( ("program", Some(matches)) => { parse_program_subcommand(matches, default_signer, wallet_manager) } + ("program-v4", Some(matches)) => { + parse_program_v4_subcommand(matches, default_signer, wallet_manager) + } ("address-lookup-table", Some(matches)) => { parse_address_lookup_table_subcommand(matches, default_signer, wallet_manager) } @@ -1103,6 +1107,11 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { process_program_subcommand(rpc_client, config, program_subcommand) } + // Deploy a custom program v4 to the chain + CliCommand::ProgramV4(program_subcommand) => { + process_program_v4_subcommand(rpc_client, config, program_subcommand) + } + // Stake Commands // Create stake account diff --git a/cli/src/program_v4.rs b/cli/src/program_v4.rs index bb1a844da..07b82636b 100644 --- a/cli/src/program_v4.rs +++ b/cli/src/program_v4.rs @@ -1,10 +1,19 @@ use { crate::{ checks::*, - cli::{log_instruction_custom_error, CliConfig, ProcessResult}, + cli::{ + log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, + ProcessResult, + }, program::calculate_max_chunk_size, }, + clap::{App, AppSettings, Arg, ArgMatches, SubCommand}, log::*, + solana_clap_utils::{ + input_parsers::{pubkey_of, pubkey_of_signer, signer_of}, + input_validators::is_valid_signer, + keypair::{DefaultSigner, SignerIndex}, + }, solana_cli_output::CliProgramId, solana_client::{ connection_cache::ConnectionCache, @@ -13,6 +22,9 @@ use { }, tpu_client::{TpuClient, TpuClientConfig}, }, + solana_program_runtime::{compute_budget::ComputeBudget, invoke_context::InvokeContext}, + solana_rbpf::{elf::Executable, verifier::RequisiteVerifier}, + solana_remote_wallet::remote_wallet::RemoteWalletManager, solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_api::config::RpcSendTransactionConfig, solana_sdk::{ @@ -29,9 +41,364 @@ use { system_instruction::{self, SystemError}, transaction::Transaction, }, - std::{cmp::Ordering, sync::Arc}, + std::{cmp::Ordering, fs::File, io::Read, rc::Rc, sync::Arc}, }; +#[derive(Debug, PartialEq, Eq)] +pub enum ProgramV4CliCommand { + Deploy { + program_location: String, + program_signer_index: SignerIndex, + authority_signer_index: SignerIndex, + }, + Redeploy { + program_location: String, + program_address: Pubkey, + buffer_signer_index: Option, + authority_signer_index: SignerIndex, + }, + Undeploy { + program_address: Pubkey, + authority_signer_index: SignerIndex, + }, + Finalize { + program_address: Pubkey, + authority_signer_index: SignerIndex, + }, +} + +pub trait ProgramV4SubCommands { + fn program_v4_subcommands(self) -> Self; +} + +impl ProgramV4SubCommands for App<'_, '_> { + fn program_v4_subcommands(self) -> Self { + self.subcommand( + SubCommand::with_name("program-v4") + .about("Program V4 management") + .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand( + SubCommand::with_name("deploy") + .about("Deploy a program") + .arg( + Arg::with_name("program_location") + .index(1) + .value_name("PROGRAM_FILEPATH") + .takes_value(true) + .help("/path/to/program.so"), + ) + .arg( + Arg::with_name("program") + .long("program") + .value_name("PROGRAM_SIGNER") + .takes_value(true) + .validator(is_valid_signer) + .help("Program account signer. The program data is written to the associated account.") + ) + .arg( + Arg::with_name("authority") + .long("authority") + .value_name("AUTHORITY_SIGNER") + .takes_value(true) + .validator(is_valid_signer) + .help("Program authority [default: the default configured keypair]") + ), + ) + .subcommand( + SubCommand::with_name("redeploy") + .about("Redeploy a previously deployed program") + .arg( + Arg::with_name("program_location") + .index(1) + .value_name("PROGRAM_FILEPATH") + .takes_value(true) + .help("/path/to/program.so"), + ) + .arg( + Arg::with_name("program-id") + .long("program-id") + .value_name("PROGRAM_ID") + .takes_value(true) + .help("Executable program's address") + ) + .arg( + Arg::with_name("buffer") + .long("buffer") + .value_name("BUFFER_SIGNER") + .takes_value(true) + .validator(is_valid_signer) + .help("Optional intermediate buffer account to write data to, which can be used to resume a failed deploy") + ) + .arg( + Arg::with_name("authority") + .long("authority") + .value_name("AUTHORITY_SIGNER") + .takes_value(true) + .validator(is_valid_signer) + .help("Program authority [default: the default configured keypair]") + ), + ) + .subcommand( + SubCommand::with_name("undeploy") + .about("Undeploy/close a program") + .arg( + Arg::with_name("program-id") + .long("program-id") + .value_name("PROGRAM_ID") + .takes_value(true) + .help("Executable program's address") + ) + .arg( + Arg::with_name("authority") + .long("authority") + .value_name("AUTHORITY_SIGNER") + .takes_value(true) + .validator(is_valid_signer) + .help("Program authority [default: the default configured keypair]") + ), + ) + .subcommand( + SubCommand::with_name("finalize") + .about("Finalize a program to make it immutable") + .arg( + Arg::with_name("program-id") + .long("program-id") + .value_name("PROGRAM_ID") + .takes_value(true) + .help("Executable program's address") + ) + .arg( + Arg::with_name("authority") + .long("authority") + .value_name("AUTHORITY_SIGNER") + .takes_value(true) + .validator(is_valid_signer) + .help("Program authority [default: the default configured keypair]") + ), + ) + ) + } +} + +pub fn parse_program_v4_subcommand( + matches: &ArgMatches<'_>, + default_signer: &DefaultSigner, + wallet_manager: &mut Option>, +) -> Result { + let (subcommand, sub_matches) = matches.subcommand(); + let response = match (subcommand, sub_matches) { + ("deploy", Some(matches)) => { + let mut bulk_signers = vec![Some( + default_signer.signer_from_path(matches, wallet_manager)?, + )]; + + let program_location = matches + .value_of("program_location") + .map(|location| location.to_string()); + + let program_pubkey = if let Ok((program_signer, Some(program_pubkey))) = + signer_of(matches, "program", wallet_manager) + { + bulk_signers.push(program_signer); + Some(program_pubkey) + } else { + pubkey_of_signer(matches, "program", wallet_manager)? + }; + + let (authority, authority_pubkey) = signer_of(matches, "authority", wallet_manager)?; + bulk_signers.push(authority); + + let signer_info = + default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; + + CliCommandInfo { + command: CliCommand::ProgramV4(ProgramV4CliCommand::Deploy { + program_location: program_location.expect("Program location is missing"), + program_signer_index: signer_info + .index_of(program_pubkey) + .expect("Program signer is missing"), + authority_signer_index: signer_info + .index_of(authority_pubkey) + .expect("Authority signer is missing"), + }), + signers: signer_info.signers, + } + } + ("redeploy", Some(matches)) => { + let mut bulk_signers = vec![Some( + default_signer.signer_from_path(matches, wallet_manager)?, + )]; + + let program_location = matches + .value_of("program_location") + .map(|location| location.to_string()); + + let buffer_pubkey = if let Ok((buffer_signer, Some(buffer_pubkey))) = + signer_of(matches, "buffer", wallet_manager) + { + bulk_signers.push(buffer_signer); + Some(buffer_pubkey) + } else { + pubkey_of_signer(matches, "buffer", wallet_manager)? + }; + + let (authority, authority_pubkey) = signer_of(matches, "authority", wallet_manager)?; + bulk_signers.push(authority); + + let signer_info = + default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; + + CliCommandInfo { + command: CliCommand::ProgramV4(ProgramV4CliCommand::Redeploy { + program_location: program_location.expect("Program location is missing"), + program_address: pubkey_of(matches, "program-id") + .expect("Program address is missing"), + buffer_signer_index: signer_info.index_of_or_none(buffer_pubkey), + authority_signer_index: signer_info + .index_of(authority_pubkey) + .expect("Authority signer is missing"), + }), + signers: signer_info.signers, + } + } + ("undeploy", Some(matches)) => { + let mut bulk_signers = vec![Some( + default_signer.signer_from_path(matches, wallet_manager)?, + )]; + + let (authority, authority_pubkey) = signer_of(matches, "authority", wallet_manager)?; + bulk_signers.push(authority); + + let signer_info = + default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; + + CliCommandInfo { + command: CliCommand::ProgramV4(ProgramV4CliCommand::Undeploy { + program_address: pubkey_of(matches, "program-id") + .expect("Program address is missing"), + authority_signer_index: signer_info + .index_of(authority_pubkey) + .expect("Authority signer is missing"), + }), + signers: signer_info.signers, + } + } + ("finalize", Some(matches)) => { + let mut bulk_signers = vec![Some( + default_signer.signer_from_path(matches, wallet_manager)?, + )]; + + let (authority, authority_pubkey) = signer_of(matches, "authority", wallet_manager)?; + bulk_signers.push(authority); + + let signer_info = + default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; + + CliCommandInfo { + command: CliCommand::ProgramV4(ProgramV4CliCommand::Finalize { + program_address: pubkey_of(matches, "program-id") + .expect("Program address is missing"), + authority_signer_index: signer_info + .index_of(authority_pubkey) + .expect("Authority signer is missing"), + }), + signers: signer_info.signers, + } + } + _ => unreachable!(), + }; + Ok(response) +} + +fn read_and_verify_elf(program_location: &str) -> Result, Box> { + let mut file = File::open(program_location) + .map_err(|err| format!("Unable to open program file: {err}"))?; + let mut program_data = Vec::new(); + file.read_to_end(&mut program_data) + .map_err(|err| format!("Unable to read program file: {err}"))?; + + // Verify the program + let program_runtime_environment = + solana_loader_v4_program::create_program_runtime_environment_v2( + &ComputeBudget::default(), + false, + ); + let executable = + Executable::::from_elf(&program_data, Arc::new(program_runtime_environment)) + .map_err(|err| format!("ELF error: {err}"))?; + + executable + .verify::() + .map_err(|err| format!("ELF error: {err}"))?; + + Ok(program_data) +} + +pub fn process_program_v4_subcommand( + rpc_client: Arc, + config: &CliConfig, + program_subcommand: &ProgramV4CliCommand, +) -> ProcessResult { + match program_subcommand { + ProgramV4CliCommand::Deploy { + program_location, + program_signer_index, + authority_signer_index, + } => { + let program_data = read_and_verify_elf(program_location)?; + let program_len = program_data.len() as u32; + + process_deploy_program( + rpc_client, + config, + &program_data, + program_len, + &config.signers[*program_signer_index].pubkey(), + Some(config.signers[*program_signer_index]), + config.signers[*authority_signer_index], + ) + } + ProgramV4CliCommand::Redeploy { + program_location, + program_address, + buffer_signer_index, + authority_signer_index, + } => { + let program_data = read_and_verify_elf(program_location)?; + let program_len = program_data.len() as u32; + let buffer_signer = buffer_signer_index.map(|index| config.signers[index]); + + process_deploy_program( + rpc_client, + config, + &program_data, + program_len, + program_address, + buffer_signer, + config.signers[*authority_signer_index], + ) + } + ProgramV4CliCommand::Undeploy { + program_address, + authority_signer_index, + } => process_undeploy_program( + rpc_client, + config, + program_address, + config.signers[*authority_signer_index], + ), + ProgramV4CliCommand::Finalize { + program_address, + authority_signer_index, + } => process_finalize_program( + rpc_client, + config, + program_address, + config.signers[*authority_signer_index], + ), + } +} + // This function can be used for the following use-cases // * Deploy a program // - buffer_signer argument must contain program signer information @@ -41,7 +408,6 @@ use { // * Redeploy a program using a buffer account // - buffer_signer argument must contain the temporary buffer account information // (program_address must contain program ID and must NOT be same as buffer_signer.pubkey()) -#[allow(dead_code)] fn process_deploy_program( rpc_client: Arc, config: &CliConfig, @@ -142,7 +508,6 @@ fn process_deploy_program( Ok(config.output_format.formatted_string(&program_id)) } -#[allow(dead_code)] fn process_undeploy_program( rpc_client: Arc, config: &CliConfig, @@ -206,7 +571,6 @@ fn process_undeploy_program( Ok(config.output_format.formatted_string(&program_id)) } -#[allow(dead_code)] fn process_finalize_program( rpc_client: Arc, config: &CliConfig, @@ -243,7 +607,6 @@ fn process_finalize_program( Ok(config.output_format.formatted_string(&program_id)) } -#[allow(dead_code)] fn check_payer( rpc_client: &RpcClient, config: &CliConfig, @@ -275,7 +638,6 @@ fn check_payer( Ok(()) } -#[allow(dead_code)] fn send_messages( rpc_client: Arc, config: &CliConfig, @@ -394,7 +756,6 @@ fn send_messages( Ok(()) } -#[allow(dead_code)] fn build_create_buffer_message( rpc_client: Arc, config: &CliConfig, @@ -563,7 +924,6 @@ fn build_retract_and_deploy_messages( Ok(messages) } -#[allow(dead_code)] fn build_retract_instruction( account: &Account, buffer_address: &Pubkey, @@ -595,7 +955,6 @@ fn build_retract_instruction( } } -#[allow(dead_code)] fn build_truncate_instructions( rpc_client: Arc, payer: &Pubkey, @@ -677,12 +1036,15 @@ fn build_truncate_instructions( mod tests { use { super::*, + crate::{clap_app::get_clap_app, cli::parse_command}, serde_json::json, solana_rpc_client_api::{ request::RpcRequest, response::{Response, RpcResponseContext}, }, - solana_sdk::signature::keypair_from_seed, + solana_sdk::signature::{ + keypair_from_seed, read_keypair_file, write_keypair_file, Keypair, + }, std::collections::HashMap, }; @@ -1001,4 +1363,225 @@ mod tests { ) .is_ok()); } + + fn make_tmp_path(name: &str) -> String { + let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); + let keypair = Keypair::new(); + + let path = format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey()); + + // whack any possible collision + let _ignored = std::fs::remove_dir_all(&path); + // whack any possible collision + let _ignored = std::fs::remove_file(&path); + + path + } + + #[test] + #[allow(clippy::cognitive_complexity)] + fn test_cli_parse_deploy() { + 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_keypair = Keypair::new(); + let program_keypair_file = make_tmp_path("program_keypair_file"); + write_keypair_file(&program_keypair, &program_keypair_file).unwrap(); + + 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-v4", + "deploy", + "/Users/test/program.so", + "--program", + &program_keypair_file, + "--authority", + &authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::ProgramV4(ProgramV4CliCommand::Deploy { + program_location: "/Users/test/program.so".to_string(), + program_signer_index: 1, + authority_signer_index: 2, + }), + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + read_keypair_file(&program_keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into() + ], + } + ); + } + + #[test] + #[allow(clippy::cognitive_complexity)] + fn test_cli_parse_redeploy() { + 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_keypair = Keypair::new(); + let program_keypair_file = make_tmp_path("program_keypair_file"); + write_keypair_file(&program_keypair, &program_keypair_file).unwrap(); + + 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-v4", + "redeploy", + "/Users/test/program.so", + "--program-id", + &program_keypair_file, + "--authority", + &authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::ProgramV4(ProgramV4CliCommand::Redeploy { + program_location: "/Users/test/program.so".to_string(), + program_address: program_keypair.pubkey(), + authority_signer_index: 1, + buffer_signer_index: None, + }), + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into() + ], + } + ); + + let buffer_keypair = Keypair::new(); + let buffer_keypair_file = make_tmp_path("buffer_keypair_file"); + write_keypair_file(&buffer_keypair, &buffer_keypair_file).unwrap(); + + let test_command = test_commands.clone().get_matches_from(vec![ + "test", + "program-v4", + "redeploy", + "/Users/test/program.so", + "--program-id", + &program_keypair_file, + "--buffer", + &buffer_keypair_file, + "--authority", + &authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::ProgramV4(ProgramV4CliCommand::Redeploy { + program_location: "/Users/test/program.so".to_string(), + program_address: program_keypair.pubkey(), + buffer_signer_index: Some(1), + authority_signer_index: 2, + }), + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + read_keypair_file(&buffer_keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into() + ], + } + ); + } + + #[test] + #[allow(clippy::cognitive_complexity)] + fn test_cli_parse_undeploy() { + 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_keypair = Keypair::new(); + let program_keypair_file = make_tmp_path("program_keypair_file"); + write_keypair_file(&program_keypair, &program_keypair_file).unwrap(); + + 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-v4", + "undeploy", + "--program-id", + &program_keypair_file, + "--authority", + &authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::ProgramV4(ProgramV4CliCommand::Undeploy { + program_address: program_keypair.pubkey(), + authority_signer_index: 1, + }), + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into() + ], + } + ); + } + + #[test] + #[allow(clippy::cognitive_complexity)] + fn test_cli_parse_finalize() { + 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_keypair = Keypair::new(); + let program_keypair_file = make_tmp_path("program_keypair_file"); + write_keypair_file(&program_keypair, &program_keypair_file).unwrap(); + + 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-v4", + "finalize", + "--program-id", + &program_keypair_file, + "--authority", + &authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_command, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::ProgramV4(ProgramV4CliCommand::Finalize { + program_address: program_keypair.pubkey(), + authority_signer_index: 1, + }), + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into() + ], + } + ); + } }