From cf33f3243c05249bf02f50d74dd717ff6b8d9a86 Mon Sep 17 00:00:00 2001 From: norwnd Date: Sat, 28 Oct 2023 16:37:17 +0300 Subject: [PATCH] cli: program deploy with offline signing (--sign-only mode) --- Cargo.lock | 2 + clap-utils/Cargo.toml | 1 + clap-utils/src/input_parsers.rs | 26 +- clap-utils/src/keypair.rs | 41 +- cli-output/src/cli_output.rs | 15 +- cli/Cargo.toml | 1 + cli/src/program.rs | 690 +++++++++++++++-------- cli/tests/program.rs | 844 ++++++++++++++++++++++++++-- docs/src/cli/deploy-a-program.md | 101 +++- remote-wallet/src/remote_keypair.rs | 1 + sdk/src/signer/mod.rs | 18 +- sdk/src/signer/null_signer.rs | 4 + sdk/src/signer/signers.rs | 7 + transaction-dos/src/main.rs | 8 +- 14 files changed, 1448 insertions(+), 311 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce44d79c7ab3ff..c506e3c7d42077 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5635,6 +5635,7 @@ dependencies = [ "assert_matches", "chrono", "clap 2.33.3", + "itertools", "rpassword", "solana-remote-wallet", "solana-sdk", @@ -5681,6 +5682,7 @@ dependencies = [ "log", "num-traits", "pretty-hex", + "rand 0.8.5", "reqwest", "semver 1.0.20", "serde", diff --git a/clap-utils/Cargo.toml b/clap-utils/Cargo.toml index c51dc0f1d4b060..2421e985ca5c35 100644 --- a/clap-utils/Cargo.toml +++ b/clap-utils/Cargo.toml @@ -19,6 +19,7 @@ thiserror = { workspace = true } tiny-bip39 = { workspace = true } uriparse = { workspace = true } url = { workspace = true } +itertools = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index f28425bf5ad28c..5979d6174b1748 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -1,7 +1,8 @@ use { crate::keypair::{ keypair_from_seed_phrase, pubkey_from_path, resolve_signer_from_path, signer_from_path, - ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG, + signer_from_path_with_config, SignerFromPathConfig, ASK_KEYWORD, + SKIP_SEED_PHRASE_VALIDATION_ARG, }, chrono::DateTime, clap::ArgMatches, @@ -118,7 +119,7 @@ pub fn pubkeys_sigs_of(matches: &ArgMatches<'_>, name: &str) -> Option, @@ -134,6 +135,27 @@ pub fn signer_of( } } +// Return a signer from matches at `name` (returns NullSigner and doesn't error). +#[allow(clippy::type_complexity)] +pub fn signer_of_allow_null_signer( + matches: &ArgMatches<'_>, + name: &str, + wallet_manager: &mut Option>, +) -> Result<(Option>, Option), Box> { + if let Some(location) = matches.value_of(name) { + // Allow pubkey signers without accompanying signatures + let config = SignerFromPathConfig { + allow_null_signer: true, + }; + let signer = + signer_from_path_with_config(matches, location, name, wallet_manager, &config)?; + let signer_pubkey = signer.pubkey(); + Ok((Some(signer), Some(signer_pubkey))) + } else { + Ok((None, None)) + } +} + pub fn pubkey_of_signer( matches: &ArgMatches<'_>, name: &str, diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index ead51c9970ea93..bd123741ce6481 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -17,6 +17,7 @@ use { }, bip39::{Language, Mnemonic, Seed}, clap::ArgMatches, + itertools::Itertools, rpassword::prompt_password, solana_remote_wallet::{ locator::{Locator as RemoteWalletLocator, LocatorError as RemoteWalletLocatorError}, @@ -66,6 +67,7 @@ impl SignOnly { } pub type CliSigners = Vec>; pub type SignerIndex = usize; +#[derive(Debug)] pub struct CliSignerInfo { pub signers: CliSigners, } @@ -196,10 +198,13 @@ impl DefaultSigner { /// `bulk_signers` is a vector of signers, all of which are optional. If any /// of those signers is `None`, then the default signer will be loaded; if /// all of those signers are `Some`, then the default signer will not be - /// loaded. + /// loaded. If multiple equivalent (same pub key) signers are provided - only + /// one of those will be returned in the result, such that NullSigner(s) + /// always get lower priority. /// /// The returned value includes all of the `bulk_signers` that were not - /// `None`, and maybe the default signer, if it was loaded. + /// `None`, and maybe the default signer (if it was loaded). There is no + /// guarantees on resulting signers ordering. /// /// # Examples /// @@ -245,17 +250,29 @@ impl DefaultSigner { wallet_manager: &mut Option>, ) -> Result> { let mut unique_signers = vec![]; - - // Determine if the default signer is needed - if bulk_signers.iter().any(|signer| signer.is_none()) { - let default_signer = self.signer_from_path(matches, wallet_manager)?; - unique_signers.push(default_signer); - } - - for signer in bulk_signers.into_iter().flatten() { - if !unique_signers.iter().any(|s| s == &signer) { - unique_signers.push(signer); + // Group provided signers by pub key + for (_, mut signers) in &bulk_signers.into_iter().group_by(|signer| -> Pubkey { + if let Some(signer) = signer { + return signer.pubkey(); + } + Pubkey::default() + }) { + let best_signer = signers.next().unwrap(); // group can't have 0 elems + if best_signer.is_none() { + // If there is a group of None signers, we need to add default one. + let default_signer = self.signer_from_path(matches, wallet_manager)?; + unique_signers.push(default_signer); + continue; // nothing else to do for this group + } + let mut best_signer = best_signer.unwrap(); // can't be None here + for signer in signers.skip(1) { + let signer = signer.unwrap(); // can't be None here + if !signer.is_null_signer() { + best_signer = signer; + break; // prefer any signer over null signer + } } + unique_signers.push(best_signer); } Ok(CliSignerInfo { signers: unique_signers, diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 7e51a05786fcfa..a0b0414179cede 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -2090,6 +2090,8 @@ impl fmt::Display for CliProgramId { #[serde(rename_all = "camelCase")] pub struct CliProgramBuffer { pub buffer: String, + pub program_data_max_len: usize, + pub min_rent_exempt_program_balance: u64, } impl QuietDisplay for CliProgramBuffer {} @@ -2097,7 +2099,17 @@ impl VerboseDisplay for CliProgramBuffer {} impl fmt::Display for CliProgramBuffer { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln_name_value(f, "Buffer:", &self.buffer) + writeln_name_value(f, "Buffer:", &self.buffer)?; + writeln_name_value( + f, + "Program data max length:", + &format!("{:?}", self.program_data_max_len), + )?; + writeln_name_value( + f, + "Min rent-exempt program balance:", + &format!("{:?}", self.min_rent_exempt_program_balance), + ) } } @@ -3154,6 +3166,7 @@ mod tests { #[test] fn test_return_signers() { + #[derive(Debug)] struct BadSigner { pubkey: Pubkey, } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 01d773ff9eaa4c..183892fc70bfdc 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -53,6 +53,7 @@ solana_rbpf = { workspace = true } spl-memo = { workspace = true, features = ["no-entrypoint"] } thiserror = { workspace = true } tiny-bip39 = { workspace = true } +rand = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/cli/src/program.rs b/cli/src/program.rs index 7a37a0a93d2571..b1e4d01f93d4ce 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -12,9 +12,16 @@ use { solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}, solana_bpf_loader_program::syscalls::create_program_runtime_environment_v1, solana_clap_utils::{ - self, hidden_unless_forced, input_parsers::*, input_validators::*, keypair::*, + self, + fee_payer::{fee_payer_arg, FEE_PAYER_ARG}, + hidden_unless_forced, + input_parsers::*, + input_validators::*, + keypair::*, + offline::{OfflineArgs, DUMP_TRANSACTION_MESSAGE, SIGN_ONLY_ARG}, }, solana_cli_output::{ + return_signers_with_config, ReturnSignersConfig, CliProgram, CliProgramAccountType, CliProgramAuthority, CliProgramBuffer, CliProgramId, CliUpgradeableBuffer, CliUpgradeableBuffers, CliUpgradeableProgram, CliUpgradeableProgramClosed, CliUpgradeableProgramExtended, CliUpgradeablePrograms, @@ -35,6 +42,7 @@ use { config::{RpcAccountInfoConfig, RpcProgramAccountsConfig, RpcSendTransactionConfig}, filter::{Memcmp, RpcFilterType}, }, + solana_rpc_client_nonce_utils::{blockhash_query, blockhash_query::BlockhashQuery}, solana_sdk::{ account::Account, account_utils::StateMut, @@ -72,18 +80,23 @@ To proceed with closing, rerun the `close` command with the `--bypass-warning` f pub enum ProgramCliCommand { Deploy { program_location: Option, + fee_payer_signer_index: SignerIndex, program_signer_index: Option, - program_pubkey: Option, buffer_signer_index: Option, - buffer_pubkey: Option, upgrade_authority_signer_index: SignerIndex, is_final: bool, max_len: Option, allow_excessive_balance: bool, skip_fee_check: bool, + sign_only: bool, + upgrade: Option, + dump_transaction_message: bool, + blockhash_query: BlockhashQuery, + min_rent_balance: Option, }, WriteBuffer { program_location: String, + fee_payer_signer_index: SignerIndex, buffer_signer_index: Option, buffer_pubkey: Option, buffer_authority_signer_index: SignerIndex, @@ -174,6 +187,14 @@ impl ProgramSubCommands for App<'_, '_> { .validator(is_valid_signer) .help("Upgrade authority [default: the default configured keypair]") ) + .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( pubkey!(Arg::with_name("program_id") .long("program-id") @@ -200,6 +221,22 @@ impl ProgramSubCommands for App<'_, '_> { .long("allow-excessive-deploy-account-balance") .takes_value(false) .help("Use the designated program id even if the account already holds a large balance of SOL") + ) + .arg(fee_payer_arg()) + .offline_args() + .arg( + Arg::with_name("upgrade") + .long("upgrade") + .takes_value(false) + .help("Run `an upgrade for existing program` instead of `initial program deploy` \ + (applies only to --sign-only mode, for other modes blockchain is consulted in real time)") + ) + .arg( + Arg::with_name("min_rent_balance") + .long("min-rent-balance") + .value_name("min_rent_balance") + .takes_value(true) + .help("Pre-computed min rent-exempt balance necessary for storing program data (required only for --sign-only mode)") ), ) .subcommand( @@ -466,37 +503,67 @@ pub fn parse_program_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 sign_only = matches.is_present(SIGN_ONLY_ARG.name); + let upgrade = if !sign_only { + None // we better query blockchain to find out whether we are doing an upgrade or an initial deploy + } else { + Some(matches.is_present("upgrade")) + }; + let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name); + let blockhash_query = BlockhashQuery::new_from_matches(matches); + let max_len = value_of(matches, "max_len"); + let min_rent_balance = value_of(matches, "min_rent_balance"); + + let mut bulk_signers = vec![]; + + let fee_payer_pubkey = if let Ok((Some(fee_payer_signer), Some(fee_payer_pubkey))) = + signer_of(matches, FEE_PAYER_ARG.name, wallet_manager) + { + bulk_signers.push(Some(fee_payer_signer)); + fee_payer_pubkey + } else { + let fee_payer_signer = default_signer.signer_from_path(matches, wallet_manager)?; + let fee_payer_pubkey = fee_payer_signer.pubkey(); + bulk_signers.push(Some(fee_payer_signer)); + fee_payer_pubkey + }; 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) + signer_of_allow_null_signer(matches, "buffer", wallet_manager) { bulk_signers.push(buffer_signer); Some(buffer_pubkey) } else { - pubkey_of_signer(matches, "buffer", wallet_manager)? + None // we'll have to generate it ourselves }; - let program_pubkey = if let Ok((program_signer, Some(program_pubkey))) = - signer_of(matches, "program_id", wallet_manager) + let (upgrade_authority, upgrade_authority_pubkey) = + signer_of(matches, "upgrade_authority", wallet_manager)?; + bulk_signers.push(upgrade_authority); + + let mut program_pubkey = if let Ok((program_signer, Some(program_pubkey))) = + signer_of_allow_null_signer(matches, "program", wallet_manager) { bulk_signers.push(program_signer); Some(program_pubkey) } else { - pubkey_of_signer(matches, "program_id", wallet_manager)? + None // we'll have to generate it ourselves }; - - let (upgrade_authority, upgrade_authority_pubkey) = - signer_of(matches, "upgrade_authority", wallet_manager)?; - bulk_signers.push(upgrade_authority); - - let max_len = value_of(matches, "max_len"); + if program_pubkey.is_none() { + // Fall back to `--program-id` parameter then, for backward-compatibility. + program_pubkey = if let Ok((program_signer, Some(program_pubkey))) = + signer_of_allow_null_signer(matches, "program_id", wallet_manager) + { + bulk_signers.push(program_signer); + Some(program_pubkey) + } else { + None // we'll have to generate it ourselves + }; + } let signer_info = default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; @@ -504,10 +571,9 @@ pub fn parse_program_subcommand( CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Deploy { program_location, + fee_payer_signer_index: signer_info.index_of(Some(fee_payer_pubkey)).unwrap(), program_signer_index: signer_info.index_of_or_none(program_pubkey), - program_pubkey, buffer_signer_index: signer_info.index_of_or_none(buffer_pubkey), - buffer_pubkey, upgrade_authority_signer_index: signer_info .index_of(upgrade_authority_pubkey) .unwrap(), @@ -515,14 +581,29 @@ pub fn parse_program_subcommand( max_len, allow_excessive_balance: matches.is_present("allow_excessive_balance"), skip_fee_check, + sign_only, + upgrade, + dump_transaction_message, + blockhash_query, + min_rent_balance, }), signers: signer_info.signers, } } ("write-buffer", Some(matches)) => { - let mut bulk_signers = vec![Some( - default_signer.signer_from_path(matches, wallet_manager)?, - )]; + let mut bulk_signers = vec![]; + + let fee_payer_pubkey = if let Ok((Some(fee_payer_signer), Some(fee_payer_pubkey))) = + signer_of(matches, "fee_payer", wallet_manager) + { + bulk_signers.push(Some(fee_payer_signer)); + fee_payer_pubkey + } else { + let fee_payer_signer = default_signer.signer_from_path(matches, wallet_manager)?; + let fee_payer_pubkey = fee_payer_signer.pubkey(); + bulk_signers.push(Some(fee_payer_signer)); + fee_payer_pubkey + }; let buffer_pubkey = if let Ok((buffer_signer, Some(buffer_pubkey))) = signer_of(matches, "buffer", wallet_manager) @@ -545,6 +626,7 @@ pub fn parse_program_subcommand( CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: matches.value_of("program_location").unwrap().to_string(), + fee_payer_signer_index: signer_info.index_of(Some(fee_payer_pubkey)).unwrap(), buffer_signer_index: signer_info.index_of_or_none(buffer_pubkey), buffer_pubkey, buffer_authority_signer_index: signer_info @@ -734,31 +816,40 @@ pub fn process_program_subcommand( match program_subcommand { ProgramCliCommand::Deploy { program_location, + fee_payer_signer_index, program_signer_index, - program_pubkey, buffer_signer_index, - buffer_pubkey, upgrade_authority_signer_index, is_final, max_len, allow_excessive_balance, skip_fee_check, + sign_only, + upgrade, + dump_transaction_message, + blockhash_query, + min_rent_balance, } => process_program_deploy( rpc_client, config, program_location, + *fee_payer_signer_index, *program_signer_index, - *program_pubkey, *buffer_signer_index, - *buffer_pubkey, *upgrade_authority_signer_index, *is_final, *max_len, *allow_excessive_balance, *skip_fee_check, + *sign_only, + *upgrade, + *dump_transaction_message, + blockhash_query, + *min_rent_balance, ), ProgramCliCommand::WriteBuffer { program_location, + fee_payer_signer_index, buffer_signer_index, buffer_pubkey, buffer_authority_signer_index, @@ -768,6 +859,7 @@ pub fn process_program_subcommand( rpc_client, config, program_location, + *fee_payer_signer_index, *buffer_signer_index, *buffer_pubkey, *buffer_authority_signer_index, @@ -879,21 +971,26 @@ fn process_program_deploy( rpc_client: Arc, config: &CliConfig, program_location: &Option, + fee_payer_signer_index: SignerIndex, program_signer_index: Option, - program_pubkey: Option, buffer_signer_index: Option, - buffer_pubkey: Option, upgrade_authority_signer_index: SignerIndex, is_final: bool, max_len: Option, allow_excessive_balance: bool, skip_fee_check: bool, + sign_only: bool, + upgrade: Option, + dump_transaction_message: bool, + blockhash_query: &BlockhashQuery, + min_rent_exempt_program_balance: Option, ) -> ProcessResult { + let fee_payer_signer = config.signers[fee_payer_signer_index]; + let upgrade_authority_signer = config.signers[upgrade_authority_signer_index]; + let (words, mnemonic, buffer_keypair) = create_ephemeral_keypair()?; let (buffer_provided, buffer_signer, buffer_pubkey) = if let Some(i) = buffer_signer_index { (true, Some(config.signers[i]), config.signers[i].pubkey()) - } else if let Some(pubkey) = buffer_pubkey { - (true, None, pubkey) } else { ( false, @@ -901,13 +998,10 @@ fn process_program_deploy( buffer_keypair.pubkey(), ) }; - let upgrade_authority_signer = config.signers[upgrade_authority_signer_index]; let default_program_keypair = get_default_program_keypair(program_location); let (program_signer, program_pubkey) = if let Some(i) = program_signer_index { (Some(config.signers[i]), config.signers[i].pubkey()) - } else if let Some(program_pubkey) = program_pubkey { - (None, program_pubkey) } else { ( Some(&default_program_keypair as &dyn Signer), @@ -915,7 +1009,11 @@ fn process_program_deploy( ) }; - let do_deploy = if let Some(account) = rpc_client + let do_initial_deploy = if let Some(upgrade) = upgrade { + !upgrade // continue with whatever user explicitly specified (mostly necessary for --sign-only mode) + } else if sign_only { + panic!("None value isn't acceptable for `upgrade` when in --sign-only mode"); + } else if let Some(account) = rpc_client .get_account_with_commitment(&program_pubkey, config.commitment)? .value { @@ -927,8 +1025,7 @@ fn process_program_deploy( } if !account.executable { - // Continue an initial deploy - true + true // continue an initial deploy } else if let Ok(UpgradeableLoaderState::Program { programdata_address, }) = account.state() @@ -955,8 +1052,7 @@ fn process_program_deploy( ) .into()); } - // Do upgrade - false + false // do upgrade } else { return Err(format!( "Program {program_pubkey} has been closed, use a new Program Id" @@ -973,16 +1069,17 @@ fn process_program_deploy( return Err(format!("{program_pubkey} is not an upgradeable program").into()); } } else { - // do new deploy - true + true // do new deploy }; - let (program_data, program_len) = if let Some(program_location) = program_location { + let (program_data, program_len) = if sign_only { + (vec![], 0) // irrelevant for sign-only mode + } else if let Some(program_location) = program_location { let program_data = read_and_verify_elf(program_location)?; let program_len = program_data.len(); (program_data, program_len) } else if buffer_provided { - // Check supplied buffer account + // Check supplied buffer account. if let Some(account) = rpc_client .get_account_with_commitment(&buffer_pubkey, config.commitment)? .value @@ -1034,51 +1131,79 @@ fn process_program_deploy( } else { return Err("Program location required if buffer not supplied".into()); }; - let programdata_len = if let Some(len) = max_len { + + let program_data_max_len = if let Some(len) = max_len { if program_len > len { return Err("Max length specified not large enough".into()); } len + } else if sign_only && !do_initial_deploy { + 0 // irrelevant for program upgrades (specifically in sign-only mode!) + } else if sign_only && do_initial_deploy { + return Err("Expected initialized max_len in sign-only mode, when performing initial program deploy (not upgrade)".into()); } else if is_final { program_len } else { program_len * 2 }; - let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption( - UpgradeableLoaderState::size_of_programdata(programdata_len), - )?; - let result = if do_deploy { - if program_signer.is_none() { - return Err( - "Initial deployments require a keypair be provided for the program id".into(), - ); - } + let (min_rent_exempt_program_balance, min_rent_exempt_program_data_balance) = if sign_only + && !do_initial_deploy + { + (0, 0) // irrelevant for program upgrades (specifically in sign-only mode!) + } else if sign_only && do_initial_deploy { + ( + min_rent_exempt_program_balance + .expect("expected initialized min_rent_exempt_program_balance"), + 0, + ) + } else { + ( + rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program())?, + rpc_client.get_minimum_balance_for_rent_exemption( + UpgradeableLoaderState::size_of_programdata(program_data_max_len), + )?, + ) + }; + + let result = if do_initial_deploy { do_process_program_write_and_deploy( rpc_client.clone(), config, &program_data, program_len, - programdata_len, - minimum_balance, + min_rent_exempt_program_balance, + program_data_max_len, + min_rent_exempt_program_data_balance, &bpf_loader_upgradeable::id(), + fee_payer_signer, Some(&[program_signer.unwrap(), upgrade_authority_signer]), buffer_signer, &buffer_pubkey, upgrade_authority_signer, allow_excessive_balance, skip_fee_check, + sign_only, + dump_transaction_message, + blockhash_query, ) } else { do_process_program_upgrade( rpc_client.clone(), config, &program_data, + program_len, + min_rent_exempt_program_data_balance, + fee_payer_signer, &program_pubkey, - config.signers[upgrade_authority_signer_index], + upgrade_authority_signer, &buffer_pubkey, buffer_signer, skip_fee_check, + sign_only, + dump_transaction_message, + blockhash_query, ) }; if result.is_ok() && is_final { @@ -1091,7 +1216,7 @@ fn process_program_deploy( None, )?; } - if result.is_err() && buffer_signer_index.is_none() { + if result.is_err() && !buffer_provided { report_ephemeral_mnemonic(words, mnemonic); } result @@ -1101,12 +1226,16 @@ fn process_write_buffer( rpc_client: Arc, config: &CliConfig, program_location: &str, + fee_payer_signer_index: SignerIndex, buffer_signer_index: Option, buffer_pubkey: Option, buffer_authority_signer_index: SignerIndex, max_len: Option, skip_fee_check: bool, ) -> ProcessResult { + let fee_payer_signer = config.signers[fee_payer_signer_index]; + let buffer_authority = config.signers[buffer_authority_signer_index]; + // Create ephemeral keypair to use for Buffer account, if not provided let (words, mnemonic, buffer_keypair) = create_ephemeral_keypair()?; let (buffer_signer, buffer_pubkey) = if let Some(i) = buffer_signer_index { @@ -1119,7 +1248,6 @@ fn process_write_buffer( buffer_keypair.pubkey(), ) }; - let buffer_authority = config.signers[buffer_authority_signer_index]; if let Some(account) = rpc_client .get_account_with_commitment(&buffer_pubkey, config.commitment)? @@ -1145,13 +1273,15 @@ fn process_write_buffer( } let program_data = read_and_verify_elf(program_location)?; - let buffer_data_len = if let Some(len) = max_len { + let buffer_data_max_len = if let Some(len) = max_len { len } else { program_data.len() }; - let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption( - UpgradeableLoaderState::size_of_programdata(buffer_data_len), + let min_rent_exempt_program_balance = rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program())?; + let min_rent_exempt_program_data_balance = rpc_client.get_minimum_balance_for_rent_exemption( + UpgradeableLoaderState::size_of_programdata(buffer_data_max_len), )?; let result = do_process_program_write_and_deploy( @@ -1159,17 +1289,21 @@ fn process_write_buffer( config, &program_data, program_data.len(), - program_data.len(), - minimum_balance, + min_rent_exempt_program_balance, + buffer_data_max_len, + min_rent_exempt_program_data_balance, &bpf_loader_upgradeable::id(), + fee_payer_signer, None, buffer_signer, &buffer_pubkey, buffer_authority, true, skip_fee_check, + false, // no need to support sign-only (typically, offline signing) for write-buffer command + false, // no need to dump transaction message for write-buffer command + &BlockhashQuery::All(blockhash_query::Source::Cluster), ); - if result.is_err() && buffer_signer_index.is_none() && buffer_signer.is_some() { report_ephemeral_mnemonic(words, mnemonic); } @@ -1881,28 +2015,39 @@ where fn do_process_program_write_and_deploy( rpc_client: Arc, config: &CliConfig, - program_data: &[u8], + program_data: &[u8], // can be empty, hence we have program_len program_len: usize, - programdata_len: usize, - minimum_balance: u64, + min_rent_exempt_program_balance: u64, + program_data_max_len: usize, + min_rent_exempt_program_data_balance: u64, loader_id: &Pubkey, + fee_payer_signer: &dyn Signer, program_signers: Option<&[&dyn Signer]>, buffer_signer: Option<&dyn Signer>, buffer_pubkey: &Pubkey, buffer_authority_signer: &dyn Signer, allow_excessive_balance: bool, skip_fee_check: bool, + sign_only: bool, + dump_transaction_message: bool, + blockhash_query: &BlockhashQuery, ) -> ProcessResult { - let blockhash = rpc_client.get_latest_blockhash()?; + let blockhash = blockhash_query.get_blockhash(&rpc_client, config.commitment)?; // Initialize buffer account or complete if already partially initialized - let (initial_instructions, balance_needed) = if let Some(account) = rpc_client + let (initial_instructions, balance_needed) = if sign_only { + // In sign-only mode assume buffer has already been initialized (we can't + // actually check it on-chain because we might not have network access, so + // we won't bother checking) - in case it's not, user will have to take care + // of it. + (vec![], 0) + } else if let Some(account) = rpc_client .get_account_with_commitment(buffer_pubkey, config.commitment)? .value { complete_partial_program_init( loader_id, - &config.signers[0].pubkey(), + &fee_payer_signer.pubkey(), buffer_pubkey, &account, if loader_id == &bpf_loader_upgradeable::id() { @@ -1910,204 +2055,252 @@ fn do_process_program_write_and_deploy( } else { program_len }, - minimum_balance, + min_rent_exempt_program_data_balance, allow_excessive_balance, )? } else if loader_id == &bpf_loader_upgradeable::id() { ( bpf_loader_upgradeable::create_buffer( - &config.signers[0].pubkey(), + &fee_payer_signer.pubkey(), buffer_pubkey, &buffer_authority_signer.pubkey(), - minimum_balance, + min_rent_exempt_program_data_balance, program_len, )?, - minimum_balance, + min_rent_exempt_program_data_balance, ) } else { ( vec![system_instruction::create_account( - &config.signers[0].pubkey(), + &fee_payer_signer.pubkey(), buffer_pubkey, - minimum_balance, + min_rent_exempt_program_data_balance, program_len as u64, loader_id, )], - minimum_balance, + min_rent_exempt_program_data_balance, ) }; let initial_message = if !initial_instructions.is_empty() { Some(Message::new_with_blockhash( &initial_instructions, - Some(&config.signers[0].pubkey()), + Some(&fee_payer_signer.pubkey()), &blockhash, )) } else { None }; - // Create and add write messages - let payer_pubkey = config.signers[0].pubkey(); - let create_msg = |offset: u32, bytes: Vec| { - let instruction = if loader_id == &bpf_loader_upgradeable::id() { - bpf_loader_upgradeable::write( - buffer_pubkey, - &buffer_authority_signer.pubkey(), - offset, - bytes, + let mut write_messages = vec![]; + if sign_only { + // In sign-only mode assume buffer has already been initialized (we can't + // actually check it on-chain because we might not have network access, so + // we won't bother checking) - in case it's not, user will have to take care + // of it. + } else { + let create_msg = |offset: u32, bytes: Vec| { + let instruction = if loader_id == &bpf_loader_upgradeable::id() { + bpf_loader_upgradeable::write( + buffer_pubkey, + &buffer_authority_signer.pubkey(), + offset, + bytes, + ) + } else { + loader_instruction::write(buffer_pubkey, loader_id, offset, bytes) + }; + Message::new_with_blockhash( + &[instruction], + Some(&fee_payer_signer.pubkey()), + &blockhash, ) - } else { - loader_instruction::write(buffer_pubkey, loader_id, offset, bytes) }; - Message::new_with_blockhash(&[instruction], Some(&payer_pubkey), &blockhash) - }; - let mut write_messages = vec![]; - let chunk_size = calculate_max_chunk_size(&create_msg); - for (chunk, i) in program_data.chunks(chunk_size).zip(0..) { - write_messages.push(create_msg((i * chunk_size) as u32, chunk.to_vec())); + let chunk_size = calculate_max_chunk_size(&create_msg); + for (chunk, i) in program_data.chunks(chunk_size).zip(0..) { + write_messages.push(create_msg((i * chunk_size) as u32, chunk.to_vec())); + } } - // Create and add final message + // Create and add final message. let final_message = if let Some(program_signers) = program_signers { let message = if loader_id == &bpf_loader_upgradeable::id() { Message::new_with_blockhash( &bpf_loader_upgradeable::deploy_with_max_program_len( - &config.signers[0].pubkey(), + &fee_payer_signer.pubkey(), &program_signers[0].pubkey(), buffer_pubkey, &program_signers[1].pubkey(), - rpc_client.get_minimum_balance_for_rent_exemption( - UpgradeableLoaderState::size_of_program(), - )?, - programdata_len, + min_rent_exempt_program_balance, + program_data_max_len, )?, - Some(&config.signers[0].pubkey()), + Some(&fee_payer_signer.pubkey()), &blockhash, ) } else { Message::new_with_blockhash( &[loader_instruction::finalize(buffer_pubkey, loader_id)], - Some(&config.signers[0].pubkey()), + Some(&fee_payer_signer.pubkey()), &blockhash, ) }; Some(message) + } else if sign_only { + panic!("Unexpected situation, program signers must not be empty in --sign-only mode"); } else { None }; - if !skip_fee_check { - check_payer( - &rpc_client, + if sign_only { + let message = final_message.expect("no final message for --sign-only mode"); + let mut tx = Transaction::new_unsigned(message); + let mut signers = program_signers + .expect("no program signers for --sign-only") + .to_vec(); + signers.push(fee_payer_signer); + // Using try_partial_sign here because fee_payer_signer might not be the fee payer we + // end up using for this transaction (it might be NullSigner). + tx.try_partial_sign(&signers, blockhash)?; + return_signers_with_config( + &tx, + &config.output_format, + &ReturnSignersConfig { + dump_transaction_message, + }, + ) + } else { + if !skip_fee_check { + check_payer( + &rpc_client, + config, + &fee_payer_signer.pubkey(), + balance_needed, + &initial_message, + &write_messages, + &final_message, + )?; + } + + send_deploy_messages( + rpc_client, + blockhash_query, config, - balance_needed, &initial_message, &write_messages, &final_message, + fee_payer_signer, + buffer_signer, + Some(buffer_authority_signer), + program_signers, )?; - } - - send_deploy_messages( - rpc_client, - config, - &initial_message, - &write_messages, - &final_message, - buffer_signer, - Some(buffer_authority_signer), - program_signers, - )?; - if let Some(program_signers) = program_signers { - let program_id = CliProgramId { - program_id: program_signers[0].pubkey().to_string(), - }; - Ok(config.output_format.formatted_string(&program_id)) - } else { - let buffer = CliProgramBuffer { - buffer: buffer_pubkey.to_string(), - }; - Ok(config.output_format.formatted_string(&buffer)) + if let Some(program_signers) = program_signers { + let program_id = CliProgramId { + program_id: program_signers[0].pubkey().to_string(), + }; + Ok(config.output_format.formatted_string(&program_id)) + } else { + let buffer = CliProgramBuffer { + buffer: buffer_pubkey.to_string(), + program_data_max_len, + min_rent_exempt_program_balance, + }; + Ok(config.output_format.formatted_string(&buffer)) + } } } fn do_process_program_upgrade( rpc_client: Arc, config: &CliConfig, - program_data: &[u8], - program_id: &Pubkey, + program_data: &[u8], // can be empty, hence we have program_len + program_len: usize, + min_rent_exempt_program_data_balance: u64, + fee_payer_signer: &dyn Signer, + program_pubkey: &Pubkey, upgrade_authority: &dyn Signer, buffer_pubkey: &Pubkey, buffer_signer: Option<&dyn Signer>, skip_fee_check: bool, + sign_only: bool, + dump_transaction_message: bool, + blockhash_query: &BlockhashQuery, ) -> ProcessResult { - let loader_id = bpf_loader_upgradeable::id(); - let data_len = program_data.len(); - let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption( - UpgradeableLoaderState::size_of_programdata(data_len), - )?; - - // Build messages to calculate fees - let blockhash = rpc_client.get_latest_blockhash()?; + let blockhash = blockhash_query.get_blockhash(&rpc_client, config.commitment)?; let (initial_message, write_messages, balance_needed) = if let Some(buffer_signer) = buffer_signer { - // Check Buffer account to see if partial initialization has occurred - let (initial_instructions, balance_needed) = if let Some(account) = rpc_client + // Check Buffer account to see if partial initialization has occurred. + let (initial_instructions, balance_needed) = if sign_only { + // In sign-only mode assume buffer has already been initialized (we can't + // actually check it on-chain because we might not have network access, so + // we won't bother checking) - in case it's not, user will have to take care + // of it. + (vec![], 0) + } else if let Some(account) = rpc_client .get_account_with_commitment(&buffer_signer.pubkey(), config.commitment)? .value { complete_partial_program_init( - &loader_id, - &config.signers[0].pubkey(), + &bpf_loader_upgradeable::id(), + &fee_payer_signer.pubkey(), &buffer_signer.pubkey(), &account, - UpgradeableLoaderState::size_of_buffer(data_len), - minimum_balance, + UpgradeableLoaderState::size_of_buffer(program_len), + min_rent_exempt_program_data_balance, true, )? } else { ( bpf_loader_upgradeable::create_buffer( - &config.signers[0].pubkey(), + &fee_payer_signer.pubkey(), buffer_pubkey, &upgrade_authority.pubkey(), - minimum_balance, - data_len, + min_rent_exempt_program_data_balance, + program_len, )?, - minimum_balance, + min_rent_exempt_program_data_balance, ) }; let initial_message = if !initial_instructions.is_empty() { Some(Message::new_with_blockhash( &initial_instructions, - Some(&config.signers[0].pubkey()), + Some(&fee_payer_signer.pubkey()), &blockhash, )) } else { None }; - let buffer_signer_pubkey = buffer_signer.pubkey(); - let upgrade_authority_pubkey = upgrade_authority.pubkey(); - let payer_pubkey = config.signers[0].pubkey(); - let create_msg = |offset: u32, bytes: Vec| { - let instruction = bpf_loader_upgradeable::write( - &buffer_signer_pubkey, - &upgrade_authority_pubkey, - offset, - bytes, - ); - Message::new_with_blockhash(&[instruction], Some(&payer_pubkey), &blockhash) - }; - // Create and add write messages let mut write_messages = vec![]; - let chunk_size = calculate_max_chunk_size(&create_msg); - for (chunk, i) in program_data.chunks(chunk_size).zip(0..) { - write_messages.push(create_msg((i * chunk_size) as u32, chunk.to_vec())); + if sign_only { + // In sign-only mode assume buffer has already been initialized (we can't + // actually check it on-chain because we might not have network access, so + // we won't bother checking) - in case it's not, user will have to take care + // of it. + } else { + let buffer_signer_pubkey = buffer_signer.pubkey(); + let upgrade_authority_pubkey = upgrade_authority.pubkey(); + let create_msg = |offset: u32, bytes: Vec| { + let instruction = bpf_loader_upgradeable::write( + &buffer_signer_pubkey, + &upgrade_authority_pubkey, + offset, + bytes, + ); + Message::new_with_blockhash( + &[instruction], + Some(&fee_payer_signer.pubkey()), + &blockhash, + ) + }; + + let chunk_size = calculate_max_chunk_size(&create_msg); + for (chunk, i) in program_data.chunks(chunk_size).zip(0..) { + write_messages.push(create_msg((i * chunk_size) as u32, chunk.to_vec())); + } } (initial_message, write_messages, balance_needed) @@ -2115,45 +2308,64 @@ fn do_process_program_upgrade( (None, vec![], 0) }; - // Create and add final message + // Create and add final message. let final_message = Message::new_with_blockhash( &[bpf_loader_upgradeable::upgrade( - program_id, + program_pubkey, buffer_pubkey, &upgrade_authority.pubkey(), - &config.signers[0].pubkey(), + &fee_payer_signer.pubkey(), )], - Some(&config.signers[0].pubkey()), + Some(&fee_payer_signer.pubkey()), &blockhash, ); let final_message = Some(final_message); - if !skip_fee_check { - check_payer( - &rpc_client, + if sign_only { + let message = final_message.expect("no final message for --sign-only mode"); + let mut tx = Transaction::new_unsigned(message); + let signers = &[fee_payer_signer, upgrade_authority]; + // Using try_partial_sign here because fee_payer_signer might not be the fee payer we + // end up using for this transaction (it might be NullSigner). + tx.try_partial_sign(signers, blockhash)?; + return_signers_with_config( + &tx, + &config.output_format, + &ReturnSignersConfig { + dump_transaction_message, + }, + ) + } else { + if !skip_fee_check { + check_payer( + &rpc_client, + config, + &fee_payer_signer.pubkey(), + balance_needed, + &initial_message, + &write_messages, + &final_message, + )?; + } + + send_deploy_messages( + rpc_client, + blockhash_query, config, - balance_needed, &initial_message, &write_messages, &final_message, + fee_payer_signer, + buffer_signer, + Some(upgrade_authority), + Some(&[upgrade_authority]), )?; - } - - send_deploy_messages( - rpc_client, - config, - &initial_message, - &write_messages, - &final_message, - buffer_signer, - Some(upgrade_authority), - Some(&[upgrade_authority]), - )?; - let program_id = CliProgramId { - program_id: program_id.to_string(), - }; - Ok(config.output_format.formatted_string(&program_id)) + let program_id = CliProgramId { + program_id: program_pubkey.to_string(), + }; + Ok(config.output_format.formatted_string(&program_id)) + } } fn read_and_verify_elf(program_location: &str) -> Result, Box> { @@ -2237,6 +2449,7 @@ fn complete_partial_program_init( fn check_payer( rpc_client: &RpcClient, config: &CliConfig, + fee_payer_pubkey: &Pubkey, balance_needed: u64, initial_message: &Option, write_messages: &[Message], @@ -2257,7 +2470,7 @@ fn check_payer( } check_account_for_spend_and_fee_with_commitment( rpc_client, - &config.signers[0].pubkey(), + &fee_payer_pubkey, balance_needed, fee, config.commitment, @@ -2267,30 +2480,30 @@ fn check_payer( fn send_deploy_messages( rpc_client: Arc, + blockhash_query: &BlockhashQuery, config: &CliConfig, initial_message: &Option, write_messages: &[Message], final_message: &Option, + fee_payer_signer: &dyn Signer, initial_signer: Option<&dyn Signer>, write_signer: Option<&dyn Signer>, final_signers: Option<&[&dyn Signer]>, ) -> Result<(), Box> { - let payer_signer = config.signers[0]; + let blockhash = blockhash_query.get_blockhash(&rpc_client, config.commitment)?; if let Some(message) = initial_message { if let Some(initial_signer) = initial_signer { trace!("Preparing the required accounts"); - let blockhash = rpc_client.get_latest_blockhash()?; - let mut initial_transaction = Transaction::new_unsigned(message.clone()); // Most of the initial_transaction combinations require both the fee-payer and new program // account to sign the transaction. One (transfer) only requires the fee-payer signature. // This check is to ensure signing does not fail on a KeypairPubkeyMismatch error from an // extraneous signature. if message.header.num_required_signatures == 2 { - initial_transaction.try_sign(&[payer_signer, initial_signer], blockhash)?; + initial_transaction.try_sign(&[fee_payer_signer, initial_signer], blockhash)?; } else { - initial_transaction.try_sign(&[payer_signer], blockhash)?; + initial_transaction.try_sign(&[fee_payer_signer], blockhash)?; } let result = rpc_client.send_and_confirm_transaction_with_spinner(&initial_transaction); log_instruction_custom_error::(result, config) @@ -2317,7 +2530,7 @@ fn send_deploy_messages( )? .send_and_confirm_messages_with_spinner( write_messages, - &[payer_signer, write_signer], + &[fee_payer_signer, write_signer], ), ConnectionCache::Quic(cache) => { let tpu_client_fut = solana_client::nonblocking::tpu_client::TpuClient::new_with_connection_cache( @@ -2335,7 +2548,7 @@ fn send_deploy_messages( rpc_client.clone(), Some(tpu_client), write_messages, - &[payer_signer, write_signer], + &[fee_payer_signer, write_signer], SendAndConfirmConfig { resign_txs_count: Some(5), with_spinner: true, @@ -2362,11 +2575,9 @@ fn send_deploy_messages( if let Some(message) = final_message { if let Some(final_signers) = final_signers { trace!("Deploying program"); - let blockhash = rpc_client.get_latest_blockhash()?; - let mut final_tx = Transaction::new_unsigned(message.clone()); let mut signers = final_signers.to_vec(); - signers.push(payer_signer); + signers.push(fee_payer_signer); final_tx.try_sign(&signers, blockhash)?; rpc_client .send_and_confirm_transaction_with_spinner_and_config( @@ -2416,7 +2627,8 @@ mod tests { }, serde_json::Value, solana_cli_output::OutputFormat, - solana_sdk::signature::write_keypair_file, + solana_rpc_client_nonce_utils::blockhash_query, + solana_sdk::signature::{write_keypair_file, NullSigner}, }; fn make_tmp_path(name: &str) -> String { @@ -2454,15 +2666,19 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some("/Users/test/program.so".to_string()), + fee_payer_signer_index: 0, buffer_signer_index: None, - buffer_pubkey: None, program_signer_index: None, - program_pubkey: None, upgrade_authority_signer_index: 0, is_final: false, max_len: None, allow_excessive_balance: false, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + min_rent_balance: None, }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } @@ -2481,15 +2697,19 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some("/Users/test/program.so".to_string()), + fee_payer_signer_index: 0, buffer_signer_index: None, - buffer_pubkey: None, program_signer_index: None, - program_pubkey: None, upgrade_authority_signer_index: 0, is_final: false, max_len: Some(42), allow_excessive_balance: false, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + min_rent_balance: None, }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } @@ -2510,15 +2730,19 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Deploy { program_location: None, + fee_payer_signer_index: 0, buffer_signer_index: Some(1), - buffer_pubkey: Some(buffer_keypair.pubkey()), program_signer_index: None, - program_pubkey: None, upgrade_authority_signer_index: 0, is_final: false, max_len: None, allow_excessive_balance: false, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + min_rent_balance: None, }), signers: vec![ read_keypair_file(&keypair_file).unwrap().into(), @@ -2541,17 +2765,24 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some("/Users/test/program.so".to_string()), + fee_payer_signer_index: 0, buffer_signer_index: None, - buffer_pubkey: None, - program_signer_index: None, - program_pubkey: Some(program_pubkey), + program_signer_index: Some(1), upgrade_authority_signer_index: 0, is_final: false, max_len: None, allow_excessive_balance: false, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + min_rent_balance: None, }), - signers: vec![read_keypair_file(&keypair_file).unwrap().into()], + signers: vec![ + read_keypair_file(&keypair_file).unwrap().into(), + Box::new(NullSigner::new(&program_pubkey)) + ], } ); @@ -2571,15 +2802,19 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some("/Users/test/program.so".to_string()), + fee_payer_signer_index: 0, buffer_signer_index: None, - buffer_pubkey: None, program_signer_index: Some(1), - program_pubkey: Some(program_keypair.pubkey()), upgrade_authority_signer_index: 0, is_final: false, max_len: None, allow_excessive_balance: false, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + min_rent_balance: None, }), signers: vec![ read_keypair_file(&keypair_file).unwrap().into(), @@ -2604,15 +2839,19 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some("/Users/test/program.so".to_string()), + fee_payer_signer_index: 0, buffer_signer_index: None, - buffer_pubkey: None, program_signer_index: None, - program_pubkey: None, upgrade_authority_signer_index: 1, is_final: false, max_len: None, allow_excessive_balance: false, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + min_rent_balance: None, }), signers: vec![ read_keypair_file(&keypair_file).unwrap().into(), @@ -2633,15 +2872,19 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some("/Users/test/program.so".to_string()), + fee_payer_signer_index: 0, buffer_signer_index: None, - buffer_pubkey: None, program_signer_index: None, - program_pubkey: None, upgrade_authority_signer_index: 0, is_final: true, max_len: None, skip_fee_check: false, allow_excessive_balance: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + min_rent_balance: None, }), signers: vec![read_keypair_file(&keypair_file).unwrap().into()], } @@ -2669,6 +2912,7 @@ mod tests { parse_command(&test_command, &default_signer, &mut None).unwrap(), CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::WriteBuffer { + fee_payer_signer_index: 0, program_location: "/Users/test/program.so".to_string(), buffer_signer_index: None, buffer_pubkey: None, @@ -2694,6 +2938,7 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: "/Users/test/program.so".to_string(), + fee_payer_signer_index: 0, buffer_signer_index: None, buffer_pubkey: None, buffer_authority_signer_index: 0, @@ -2721,6 +2966,7 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: "/Users/test/program.so".to_string(), + fee_payer_signer_index: 0, buffer_signer_index: Some(1), buffer_pubkey: Some(buffer_keypair.pubkey()), buffer_authority_signer_index: 0, @@ -2751,6 +2997,7 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: "/Users/test/program.so".to_string(), + fee_payer_signer_index: 0, buffer_signer_index: None, buffer_pubkey: None, buffer_authority_signer_index: 1, @@ -2786,6 +3033,7 @@ mod tests { CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: "/Users/test/program.so".to_string(), + fee_payer_signer_index: 0, buffer_signer_index: Some(1), buffer_pubkey: Some(buffer_keypair.pubkey()), buffer_authority_signer_index: 2, @@ -3315,15 +3563,19 @@ mod tests { rpc_client: Some(Arc::new(RpcClient::new_mock("".to_string()))), command: CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(program_location.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, buffer_signer_index: None, - buffer_pubkey: None, program_signer_index: None, - program_pubkey: None, upgrade_authority_signer_index: 0, is_final: false, max_len: None, allow_excessive_balance: false, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + min_rent_balance: None, }), signers: vec![&default_keypair], output_format: OutputFormat::JsonCompact, diff --git a/cli/tests/program.rs b/cli/tests/program.rs index 5bd10a92b7f6a5..7a8a8ba1b1e777 100644 --- a/cli/tests/program.rs +++ b/cli/tests/program.rs @@ -1,6 +1,7 @@ #![allow(clippy::arithmetic_side_effects)] use { + rand::Rng, serde_json::Value, solana_cli::{ cli::{process_command, CliCommand, CliConfig}, @@ -10,12 +11,14 @@ use { solana_cli_output::OutputFormat, solana_faucet::faucet::run_local_faucet, solana_rpc_client::rpc_client::RpcClient, + solana_rpc_client_nonce_utils::blockhash_query::BlockhashQuery, solana_sdk::{ account_utils::StateMut, bpf_loader_upgradeable::{self, UpgradeableLoaderState}, + clock::Slot, commitment_config::CommitmentConfig, pubkey::Pubkey, - signature::{Keypair, Signer}, + signature::{Keypair, NullSigner, Presigner, Signature, Signer}, }, solana_streamer::socket::SocketAddrSpace, solana_test_validator::TestValidator, @@ -63,17 +66,22 @@ fn test_cli_program_deploy_non_upgradeable() { }; process_command(&config).unwrap(); + // Deploy a program config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_path.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, program_signer_index: None, - program_pubkey: None, buffer_signer_index: None, - buffer_pubkey: None, allow_excessive_balance: false, upgrade_authority_signer_index: 0, is_final: true, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); config.output_format = OutputFormat::JsonCompact; let response = process_command(&config); @@ -109,15 +117,19 @@ fn test_cli_program_deploy_non_upgradeable() { config.signers = vec![&keypair, &custom_address_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(1), - program_pubkey: None, buffer_signer_index: None, - buffer_pubkey: None, allow_excessive_balance: false, upgrade_authority_signer_index: 0, is_final: true, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); process_command(&config).unwrap(); let account1 = rpc_client @@ -163,15 +175,19 @@ fn test_cli_program_deploy_non_upgradeable() { config.signers = vec![&keypair, &custom_address_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(1), - program_pubkey: None, buffer_signer_index: None, - buffer_pubkey: None, allow_excessive_balance: false, upgrade_authority_signer_index: 0, is_final: true, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); let err = process_command(&config).unwrap_err(); assert_eq!( @@ -185,15 +201,19 @@ fn test_cli_program_deploy_non_upgradeable() { // Use forcing parameter to deploy to account with excess balance 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(1), - program_pubkey: None, buffer_signer_index: None, - buffer_pubkey: None, allow_excessive_balance: true, upgrade_authority_signer_index: 0, is_final: true, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); process_command(&config).unwrap_err(); } @@ -245,15 +265,19 @@ fn test_cli_program_deploy_no_authority() { config.signers = vec![&keypair, &upgrade_authority]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_path.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, program_signer_index: None, - program_pubkey: None, buffer_signer_index: None, - buffer_pubkey: None, allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: true, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); config.output_format = OutputFormat::JsonCompact; let response = process_command(&config); @@ -268,18 +292,23 @@ fn test_cli_program_deploy_no_authority() { let program_id = Pubkey::from_str(program_id_str).unwrap(); // Attempt to upgrade the program - config.signers = vec![&keypair, &upgrade_authority]; + let program_signer = NullSigner::new(&program_id); + config.signers = vec![&keypair, &upgrade_authority, &program_signer]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_path.to_str().unwrap().to_string()), - program_signer_index: None, - program_pubkey: Some(program_id), + fee_payer_signer_index: 0, + program_signer_index: Some(2), buffer_signer_index: None, - buffer_pubkey: None, allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: false, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); process_command(&config).unwrap_err(); } @@ -332,15 +361,19 @@ fn test_cli_program_deploy_with_authority() { 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, allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: false, max_len: Some(max_len), skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); config.output_format = OutputFormat::JsonCompact; let response = process_command(&config); @@ -380,15 +413,19 @@ fn test_cli_program_deploy_with_authority() { config.signers = vec![&keypair, &upgrade_authority]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_path.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, program_signer_index: None, - program_pubkey: None, 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, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -419,18 +456,23 @@ fn test_cli_program_deploy_with_authority() { ); // Upgrade the program - config.signers = vec![&keypair, &upgrade_authority]; + let program_signer = NullSigner::new(&program_pubkey); + config.signers = vec![&keypair, &upgrade_authority, &program_signer]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_path.to_str().unwrap().to_string()), - program_signer_index: None, - program_pubkey: Some(program_pubkey), + fee_payer_signer_index: 0, + program_signer_index: Some(2), 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, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); process_command(&config).unwrap(); let program_account = rpc_client.get_account(&program_pubkey).unwrap(); @@ -474,18 +516,23 @@ fn test_cli_program_deploy_with_authority() { ); // Upgrade with new authority - config.signers = vec![&keypair, &new_upgrade_authority]; + let program_signer = NullSigner::new(&program_pubkey); + config.signers = vec![&keypair, &new_upgrade_authority, &program_signer]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_path.to_str().unwrap().to_string()), - program_signer_index: None, - program_pubkey: Some(program_pubkey), + fee_payer_signer_index: 0, + program_signer_index: Some(2), buffer_signer_index: None, - buffer_pubkey: None, allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: false, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); process_command(&config).unwrap(); let program_account = rpc_client.get_account(&program_pubkey).unwrap(); @@ -549,18 +596,23 @@ fn test_cli_program_deploy_with_authority() { assert_eq!(new_upgrade_authority_str, "none"); // Upgrade with no authority - config.signers = vec![&keypair, &new_upgrade_authority]; + let program_signer = NullSigner::new(&program_pubkey); + config.signers = vec![&keypair, &new_upgrade_authority, &program_signer]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_path.to_str().unwrap().to_string()), - program_signer_index: None, - program_pubkey: Some(program_pubkey), + fee_payer_signer_index: 0, + program_signer_index: Some(2), buffer_signer_index: None, - buffer_pubkey: None, allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: false, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); process_command(&config).unwrap_err(); @@ -568,15 +620,19 @@ fn test_cli_program_deploy_with_authority() { config.signers = vec![&keypair, &new_upgrade_authority]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_path.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, program_signer_index: None, - program_pubkey: None, buffer_signer_index: None, - buffer_pubkey: None, allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: true, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); let response = process_command(&config); let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); @@ -671,15 +727,19 @@ fn test_cli_program_close_program() { 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, allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: false, max_len: Some(max_len), skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); config.output_format = OutputFormat::JsonCompact; process_command(&config).unwrap(); @@ -864,6 +924,7 @@ fn test_cli_program_write_buffer() { config.signers = vec![&keypair]; config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: noop_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, buffer_signer_index: None, buffer_pubkey: None, buffer_authority_signer_index: 0, @@ -899,6 +960,7 @@ fn test_cli_program_write_buffer() { config.signers = vec![&keypair, &buffer_keypair]; config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: noop_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, buffer_signer_index: Some(1), buffer_pubkey: Some(buffer_keypair.pubkey()), buffer_authority_signer_index: 0, @@ -961,6 +1023,7 @@ fn test_cli_program_write_buffer() { config.signers = vec![&keypair, &buffer_keypair, &authority_keypair]; config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: noop_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, buffer_signer_index: Some(1), buffer_pubkey: Some(buffer_keypair.pubkey()), buffer_authority_signer_index: 2, @@ -999,6 +1062,7 @@ fn test_cli_program_write_buffer() { config.signers = vec![&keypair, &buffer_keypair, &authority_keypair]; config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: noop_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, buffer_signer_index: None, buffer_pubkey: None, buffer_authority_signer_index: 2, @@ -1073,6 +1137,7 @@ fn test_cli_program_write_buffer() { config.signers = vec![&keypair]; config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: noop_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, buffer_signer_index: None, buffer_pubkey: None, buffer_authority_signer_index: 0, @@ -1114,6 +1179,7 @@ fn test_cli_program_write_buffer() { config.signers = vec![&keypair, &buffer_keypair]; config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: noop_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, buffer_signer_index: Some(1), buffer_pubkey: Some(buffer_keypair.pubkey()), buffer_authority_signer_index: 0, @@ -1124,15 +1190,19 @@ fn test_cli_program_write_buffer() { config.signers = vec![&keypair, &buffer_keypair]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_large_path.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, program_signer_index: None, - program_pubkey: None, buffer_signer_index: Some(1), - buffer_pubkey: Some(buffer_keypair.pubkey()), allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: true, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); config.output_format = OutputFormat::JsonCompact; let error = process_command(&config).unwrap_err(); @@ -1186,6 +1256,7 @@ fn test_cli_program_set_buffer_authority() { config.signers = vec![&keypair, &buffer_keypair]; config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: noop_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, buffer_signer_index: Some(1), buffer_pubkey: Some(buffer_keypair.pubkey()), buffer_authority_signer_index: 0, @@ -1302,6 +1373,7 @@ fn test_cli_program_mismatch_buffer_authority() { config.signers = vec![&keypair, &buffer_keypair, &buffer_authority]; config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: noop_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, buffer_signer_index: Some(1), buffer_pubkey: Some(buffer_keypair.pubkey()), buffer_authority_signer_index: 2, @@ -1317,39 +1389,529 @@ fn test_cli_program_mismatch_buffer_authority() { } // Attempt to deploy with mismatched authority + let buffer_signer = NullSigner::new(&buffer_keypair.pubkey()); let upgrade_authority = Keypair::new(); - config.signers = vec![&keypair, &upgrade_authority]; + config.signers = vec![&keypair, &upgrade_authority, &buffer_signer]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_path.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, program_signer_index: None, - program_pubkey: None, - buffer_signer_index: None, - buffer_pubkey: Some(buffer_keypair.pubkey()), + buffer_signer_index: Some(2), allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: true, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); process_command(&config).unwrap_err(); // Attempt to deploy matched authority - config.signers = vec![&keypair, &buffer_authority]; + let buffer_signer = NullSigner::new(&buffer_keypair.pubkey()); + config.signers = vec![&keypair, &buffer_authority, &buffer_signer]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(noop_path.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, program_signer_index: None, - program_pubkey: None, - buffer_signer_index: None, - buffer_pubkey: Some(buffer_keypair.pubkey()), + buffer_signer_index: Some(2), allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: true, max_len: None, skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); process_command(&config).unwrap(); } +#[test] +fn test_cli_program_deploy_with_offline_signing() { + 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 mut noop_large_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + noop_large_path.push("tests"); + noop_large_path.push("fixtures"); + noop_large_path.push("noop_large"); + noop_large_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 blockhash = rpc_client.get_latest_blockhash().unwrap(); + + let minimum_balance_for_program = rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()) + .unwrap(); + let mut file = File::open(noop_large_path.to_str().unwrap()).unwrap(); + let mut large_program_data = Vec::new(); + file.read_to_end(&mut large_program_data).unwrap(); + let max_program_data_len = large_program_data.len(); + let minimum_balance_for_large_buffer = rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_programdata( + max_program_data_len, + )) + .unwrap(); + + let mut config = CliConfig::recent_for_tests(); + config.json_rpc_url = test_validator.rpc_url(); + + let online_signer = Keypair::new(); + let online_signer_identity = NullSigner::new(&online_signer.pubkey()); + let offline_signer = Keypair::new(); + let buffer_signer = Keypair::new(); + let buffer_signer_identity = NullSigner::new(&buffer_signer.pubkey()); + // For simplicity, keypair for program signer should be different from online signer or + // offline signer keypairs. + let program_signer = Keypair::new(); + let program_signer_identity = NullSigner::new(&program_signer.pubkey()); + + // Assume fee payer will be either online signer or offline signer (could be completely + // separate signer too, but that option is unlikely to be chosen often, so don't bother + // testing for it), we want to test both + let use_offline_signer_as_fee_payer = rand::thread_rng().gen_bool(0.5); + + config.command = CliCommand::Airdrop { + pubkey: None, + lamports: 100 * minimum_balance_for_large_buffer, // gotta be enough for this test + }; + config.signers = vec![&online_signer]; + process_command(&config).unwrap(); + config.command = CliCommand::Airdrop { + pubkey: None, + lamports: 100 * minimum_balance_for_large_buffer, // gotta be enough for this test + }; + config.signers = vec![&offline_signer]; + process_command(&config).unwrap(); + + create_buffer_with_offline_authority( + &rpc_client, + &noop_path, + &mut config, + &online_signer, + &offline_signer, + &buffer_signer, + &buffer_signer_identity, + minimum_balance_for_program, + ); + + // Offline sign-only (signature over wrong max_len) + config.signers = vec![ + &offline_signer, + &buffer_signer_identity, + &program_signer_identity, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer_identity); // can't (and won't) provide signature, for simplicity + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: Some(max_program_data_len + 1), // will ensure offline signature applies to wrong(different) message + skip_fee_check: false, + sign_only: true, + upgrade: Some(false), + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: Some(minimum_balance_for_program), + }); + config.output_format = OutputFormat::JsonCompact; + let output = process_command(&config).unwrap(); + let offline_pre_signer = fetch_pre_signer(&output, &offline_signer.pubkey().to_string()); + + // Attempt to deploy from buffer using signature over wrong(different) message (should fail) + config.signers = vec![ + &offline_pre_signer, + &buffer_signer_identity, + &program_signer, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer); // can (and will) provide signature + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: Some(max_program_data_len), + skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: Some(minimum_balance_for_program), + }); + config.output_format = OutputFormat::JsonCompact; + let error = process_command(&config).unwrap_err(); + assert_eq!(error.to_string(), "presigner error"); + + // Offline sign-only (signature over wrong min_rent_balance) + config.signers = vec![ + &offline_signer, + &buffer_signer_identity, + &program_signer_identity, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer_identity); // can't (and won't) provide signature, for simplicity + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: Some(max_program_data_len), + skip_fee_check: false, + sign_only: true, + upgrade: Some(false), + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: Some(minimum_balance_for_program + 1), // will ensure offline signature applies to wrong(different) message + }); + config.output_format = OutputFormat::JsonCompact; + let output = process_command(&config).unwrap(); + let offline_pre_signer = fetch_pre_signer(&output, &offline_signer.pubkey().to_string()); + + // Attempt to deploy from buffer using signature over wrong(different) message (should fail) + config.signers = vec![ + &offline_pre_signer, + &buffer_signer_identity, + &program_signer, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer); // can (and will) provide signature + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: Some(max_program_data_len), + skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: Some(minimum_balance_for_program), + }); + config.output_format = OutputFormat::JsonCompact; + let error = process_command(&config).unwrap_err(); + assert_eq!(error.to_string(), "presigner error"); + + // Offline sign-only (signature over upgrade when program hasn't even been deployed yet) + config.signers = vec![ + &offline_signer, + &buffer_signer_identity, + &program_signer_identity, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer_identity); // can't (and won't) provide signature, for simplicity + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: Some(max_program_data_len), + skip_fee_check: false, + sign_only: true, + upgrade: Some(true), // will ensure offline signature applies to wrong(different) message + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: Some(minimum_balance_for_program), + }); + config.output_format = OutputFormat::JsonCompact; + let output = process_command(&config).unwrap(); + let offline_pre_signer = fetch_pre_signer(&output, &offline_signer.pubkey().to_string()); + + // Attempt to deploy from buffer using signature over wrong(different) message (should fail) + config.signers = vec![ + &offline_pre_signer, + &buffer_signer_identity, + &program_signer, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer); // can (and will) provide signature + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: Some(max_program_data_len), + skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: Some(minimum_balance_for_program), + }); + config.output_format = OutputFormat::JsonCompact; + let error = process_command(&config).unwrap_err(); + assert_eq!(error.to_string(), "presigner error"); + + // Offline sign-only with online signer as fee payer (correct signature for initial program deploy) + config.signers = vec![ + &offline_signer, + &buffer_signer_identity, + &program_signer_identity, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer_identity); // can't (and won't) provide signature, for simplicity + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: Some(max_program_data_len), + skip_fee_check: false, + sign_only: true, + upgrade: Some(false), + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: Some(minimum_balance_for_program), + }); + config.output_format = OutputFormat::JsonCompact; + let output = process_command(&config).unwrap(); + let offline_pre_signer = fetch_pre_signer(&output, &offline_signer.pubkey().to_string()); + + // Attempt to deploy from buffer using signature over correct message (should succeed) + config.signers = vec![ + &offline_pre_signer, + &buffer_signer_identity, + &program_signer, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer); // can (and will) provide signature + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: Some(max_program_data_len), + skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: Some(minimum_balance_for_program), + }); + config.output_format = OutputFormat::JsonCompact; + + verify_deployed_program( + &rpc_client, + &mut config, + &online_signer, + offline_signer.pubkey(), + program_signer.pubkey(), + max_program_data_len, + ); + + create_buffer_with_offline_authority( + &rpc_client, + &noop_large_path, + &mut config, + &online_signer, + &offline_signer, + &buffer_signer, + &buffer_signer_identity, + minimum_balance_for_program, + ); // prepare buffer to upgrade deployed program (to larger program) + + // Offline sign-only (signature over initial deploy when program has already been deployed yet) + config.signers = vec![ + &offline_signer, + &buffer_signer_identity, + &program_signer_identity, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer_identity); // can't (and won't) provide signature, for simplicity + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: Some(max_program_data_len), // have to specify this here to satisfy `upgrade = false` mode + skip_fee_check: false, + sign_only: true, + upgrade: Some(false), // will ensure offline signature applies to wrong(different) message + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: Some(minimum_balance_for_program), // have to specify this here to satisfy `upgrade = false` mode + }); + config.output_format = OutputFormat::JsonCompact; + let output = process_command(&config).unwrap(); + let offline_pre_signer = fetch_pre_signer(&output, &offline_signer.pubkey().to_string()); + + // Attempt to deploy from buffer using signature over wrong(different) message (should fail) + config.signers = vec![ + &offline_pre_signer, + &buffer_signer_identity, + &program_signer, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer); // can (and will) provide signature + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: None, + skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: None, + }); + config.output_format = OutputFormat::JsonCompact; + let error = process_command(&config).unwrap_err(); + assert_eq!(error.to_string(), "presigner error"); + + // Offline sign-only with online signer as fee payer (correct signature for program upgrade) + config.signers = vec![ + &offline_signer, + &buffer_signer_identity, + &program_signer_identity, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer_identity); // can't (and won't) provide signature, for simplicity + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: None, + skip_fee_check: false, + sign_only: true, + upgrade: Some(true), + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: None, + }); + config.output_format = OutputFormat::JsonCompact; + let output = process_command(&config).unwrap(); + let offline_pre_signer = fetch_pre_signer(&output, &offline_signer.pubkey().to_string()); + + // Attempt to deploy from buffer using signature over correct message (should succeed) + config.signers = vec![ + &offline_pre_signer, + &buffer_signer_identity, + &program_signer, + ]; + let mut fee_payer_signer_index = 0; // offline signer + if !use_offline_signer_as_fee_payer { + fee_payer_signer_index = 3; // online signer + config.signers.push(&online_signer); // can (and will) provide signature + } + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index, + program_signer_index: Some(2), + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: None, + skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::new(Some(blockhash), true, None), + min_rent_balance: None, + }); + config.output_format = OutputFormat::JsonCompact; + + verify_deployed_program( + &rpc_client, + &mut config, + &online_signer, + offline_signer.pubkey(), + program_signer.pubkey(), + max_program_data_len, + ); +} + #[test] fn test_cli_program_show() { solana_logger::setup(); @@ -1398,6 +1960,7 @@ fn test_cli_program_show() { config.signers = vec![&keypair, &buffer_keypair, &authority_keypair]; config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: noop_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, buffer_signer_index: Some(1), buffer_pubkey: Some(buffer_keypair.pubkey()), buffer_authority_signer_index: 2, @@ -1454,15 +2017,19 @@ fn test_cli_program_show() { config.signers = vec![&keypair, &authority_keypair, &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, allow_excessive_balance: false, upgrade_authority_signer_index: 1, is_final: false, max_len: Some(max_len), skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); config.output_format = OutputFormat::JsonCompact; let min_slot = rpc_client.get_slot().unwrap(); @@ -1585,6 +2152,7 @@ fn test_cli_program_dump() { config.signers = vec![&keypair, &buffer_keypair, &authority_keypair]; config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { program_location: noop_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, buffer_signer_index: Some(1), buffer_pubkey: Some(buffer_keypair.pubkey()), buffer_authority_signer_index: 2, @@ -1614,3 +2182,185 @@ fn test_cli_program_dump() { assert_eq!(program_data[i], out_data[i]); } } + +fn fetch_pre_signer(cmd_output_json_str: &String, target_pub_key: &String) -> Presigner { + let json: Value = serde_json::from_str(&cmd_output_json_str).unwrap(); + let target_pre_signer_str = json + .as_object() + .unwrap() + .get("signers") + .unwrap() + .as_array() + .unwrap() + .iter() + .find(|pre_signer| pre_signer.as_str().unwrap().starts_with(target_pub_key)) + .unwrap() + .as_str() + .unwrap(); + let mut parts = target_pre_signer_str.split("="); + assert_eq!(2, parts.clone().count()); + Presigner::new( + &Pubkey::from_str(parts.next().unwrap()).unwrap(), + &Signature::from_str(parts.next().unwrap()).unwrap(), + ) +} + +fn create_buffer_with_offline_authority<'a>( + rpc_client: &RpcClient, + program_path: &PathBuf, + config: &mut CliConfig<'a>, + online_signer: &'a Keypair, + offline_signer: &'a Keypair, + buffer_signer: &'a Keypair, + buffer_signer_identity: &'a NullSigner, + minimum_balance_for_program: u64, +) { + // Write a buffer + config.signers = vec![online_signer, buffer_signer]; + config.command = CliCommand::Program(ProgramCliCommand::WriteBuffer { + program_location: program_path.to_str().unwrap().to_string(), + fee_payer_signer_index: 0, + buffer_signer_index: Some(1), + buffer_pubkey: Some(buffer_signer.pubkey()), + buffer_authority_signer_index: 0, + max_len: None, + skip_fee_check: false, + }); + process_command(&config).unwrap(); + let buffer_account = rpc_client.get_account(&buffer_signer.pubkey()).unwrap(); + if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + assert_eq!(authority_address, Some(online_signer.pubkey())); + } else { + panic!("not a buffer account"); + } + + // Set buffer authority to offline signer + config.signers = vec![online_signer, buffer_signer]; + config.command = CliCommand::Program(ProgramCliCommand::SetBufferAuthority { + buffer_pubkey: buffer_signer.pubkey(), + buffer_authority_index: Some(0), + new_buffer_authority: offline_signer.pubkey(), + }); + config.output_format = OutputFormat::JsonCompact; + let response = process_command(&config); + let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); + let offline_signer_authority_str = json + .as_object() + .unwrap() + .get("authority") + .unwrap() + .as_str() + .unwrap(); + assert_eq!( + Pubkey::from_str(offline_signer_authority_str).unwrap(), + offline_signer.pubkey() + ); + let buffer_account = rpc_client.get_account(&buffer_signer.pubkey()).unwrap(); + if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + assert_eq!(authority_address, Some(offline_signer.pubkey())); + } else { + panic!("not a buffer account"); + } + + // Attempt to deploy from buffer using previous authority (should fail) + config.signers = vec![online_signer, buffer_signer_identity]; + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: None, + fee_payer_signer_index: 0, + program_signer_index: None, + buffer_signer_index: Some(1), + allow_excessive_balance: false, + upgrade_authority_signer_index: 0, + is_final: false, + max_len: None, + skip_fee_check: false, + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + min_rent_balance: Some(minimum_balance_for_program), + }); + config.output_format = OutputFormat::JsonCompact; + let error = process_command(&config).unwrap_err(); + assert_eq!(error.to_string(), "Deploying program failed: RPC response error -32002: Transaction simulation failed: Error processing Instruction 1: Incorrect authority provided [5 log messages]"); +} + +fn verify_deployed_program<'a>( + rpc_client: &RpcClient, + config: &mut CliConfig<'a>, + online_signer: &'a Keypair, + offline_signer_pub_key: Pubkey, + program_signer_pub_key: Pubkey, + max_program_data_len: usize, +) { + let min_slot = rpc_client.get_slot().unwrap(); + process_command(&config).unwrap(); + let max_slot = rpc_client.get_slot().unwrap(); + + // Verify show + config.signers = vec![online_signer]; + config.command = CliCommand::Program(ProgramCliCommand::Show { + account_pubkey: Some(program_signer_pub_key), + authority_pubkey: program_signer_pub_key, + get_programs: false, + get_buffers: false, + all: false, + use_lamports_unit: false, + }); + let response = process_command(&config); + let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); + let address_str = json + .as_object() + .unwrap() + .get("programId") + .unwrap() + .as_str() + .unwrap(); + assert_eq!( + program_signer_pub_key, + Pubkey::from_str(address_str).unwrap() + ); + let programdata_address_str = json + .as_object() + .unwrap() + .get("programdataAddress") + .unwrap() + .as_str() + .unwrap(); + let (programdata_pubkey, _) = Pubkey::find_program_address( + &[program_signer_pub_key.as_ref()], + &bpf_loader_upgradeable::id(), + ); + assert_eq!( + programdata_pubkey, + Pubkey::from_str(programdata_address_str).unwrap() + ); + let authority_str = json + .as_object() + .unwrap() + .get("authority") + .unwrap() + .as_str() + .unwrap(); + assert_eq!( + offline_signer_pub_key, + Pubkey::from_str(authority_str).unwrap() + ); + let deployed_slot = json + .as_object() + .unwrap() + .get("lastDeploySlot") + .unwrap() + .as_u64() + .unwrap(); + assert!(deployed_slot >= min_slot); + assert!(deployed_slot <= max_slot); + let data_len = json + .as_object() + .unwrap() + .get("dataLen") + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(max_program_data_len, data_len as usize); +} diff --git a/docs/src/cli/deploy-a-program.md b/docs/src/cli/deploy-a-program.md index 1f39dfe1399e70..8ecbcc026aa6bd 100644 --- a/docs/src/cli/deploy-a-program.md +++ b/docs/src/cli/deploy-a-program.md @@ -13,8 +13,8 @@ To deploy a program, use the Solana tools to interact with the on-chain loader to: - Initialize a program account -- Upload the program's shared object to the program account's data buffer -- Verify the uploaded program +- Upload the program's shared object (the program binary .so) to the program account's data buffer +- (optional) Verify the uploaded program - Finalize the program by marking the program account executable. Once deployed, anyone can execute the program by sending transactions that @@ -25,7 +25,7 @@ reference it to the cluster. ### Deploy a program To deploy a program, you will need the location of the program's shared object -(the program binary .so) +(the program binary .so): ```bash solana program deploy @@ -48,9 +48,9 @@ If the program id is not specified on the command line the tools will first look for a keypair file matching the ``, or internally generate a new keypair. -A matching program keypair file is in the same directory as the program's shared -object, and named -keypair.json. Matching program keypairs are -generated automatically by the program build tools: +You can typically find a matching program keypair file is in the same directory +as the program's shared object, and named -keypair.json. Matching +program keypairs are generated automatically by the program build tools: ```bash ./path-to-program/program.so @@ -89,10 +89,10 @@ Data Length: 5216 (0x1460) bytes ### Redeploy a program A program can be redeployed to the same address to facilitate rapid development, -bug fixes, or upgrades. Matching keypair files are generated once so that -redeployments will be to the same program address. +bug fixes, or upgrades. -The command looks the same as the deployment command: +The command looks the same as the deployment command (same keypair file that resides from file directory corresponding +to , this keypair file will be generated once and then reused): ```bash solana program deploy @@ -111,8 +111,8 @@ solana program deploy --max-len 200000 Note that program accounts are required to be [rent-exempt](developing/programming-model/accounts.md#rent-exemption), and the -`max-len` is fixed after initial deployment, so any SOL in the program accounts -is locked up permanently. +`max-len` **cannot be changed** after initial deployment, yet any SOL in the program accounts +is locked up permanently and cannot be reclaimed. ### Resuming a failed deploy @@ -157,7 +157,7 @@ solana program deploy --buffer Both program and buffer accounts can be closed and their lamport balances transferred to a recipient's account. -If deployment fails there will be a left over buffer account that holds +If deployment fails there will be a left-over buffer account that holds lamports. The buffer account can either be used to [resume a deploy](#resuming-a-failed-deploy) or closed. @@ -256,12 +256,12 @@ solana program dump ``` The dumped file will be in the same as what was deployed, so in the case of a -shared object, the dumped file will be a fully functional shared object. Note +shared object (the program binary .so), the dumped file will be a fully functional shared object. Note that the `dump` command dumps the entire data space, which means the output file -will have trailing zeros after the shared object's data up to `max_len`. +might have trailing zeros after the shared object's data up to `max_len`. Sometimes it is useful to dump and compare a program to ensure it matches a -known program binary. The original program file can be zero-extended, hashed, -and compared to the hash of the dumped file. +known program binary. The dumped file can be zero-truncated, hashed, +and compared to the hash of the original program file. ```bash $ solana dump dump.so @@ -275,13 +275,14 @@ $ sha256sum extended.so dump.so Instead of deploying directly to the program account, the program can be written to an intermediary buffer account. Intermediary accounts can be useful for things like multi-entity governed programs where the governing members fist verify the -intermediary buffer contents and then vote to allow an upgrade using it. +intermediary buffer contents and then vote to allow an upgrade using it, or for +[deploying programs with offline signer authority](#deploying-program-with-offline-signer-authority). ```bash solana program write-buffer ``` -Buffer accounts support authorities like program accounts: +Buffer accounts support different authorities (including offline signer and program account signer): ```bash solana program set-buffer-authority --new-buffer-authority @@ -297,6 +298,68 @@ the program: solana program deploy --program-id --buffer ``` -Note, the buffer's authority must match the program's upgrade authority. +Note, the buffer's authority must match the program's upgrade authority. Also, upon successful deploy +buffer accounts contents are copied into program accounts and are erased from blockchain. Buffers also support `show` and `dump` just like programs do. + +### Managing (deploying/upgrading) program using offline signer as authority + +Storing private key(s) on machine without internet access (offline signer) is much safer compared to +storing them on machine with internet access (online signer), this section describes how to do that. + +Below we assume (for simplicity) a setup with 3 different pairs of keys: +- online signer (used as fee payer for deploying program buffer, deploying/upgrading program itself) +- offline signer (serves as authority over program deploys/upgrades, protects program deploys/upgrades + from certain types of attacks) +- program signer (typically, generated when program has been compiled into `.so` file) + +The general process is as follows: +1) (use online machine) create buffer +2) (use online machine) upgrade buffer authority to offline signer +3) (optional, use a separate online machine) verify the actual buffer on-chain contents +4) (use offline machine) get a signature for your intent to create or upgrade program +5) (use online machine) use this signature to build and broadcast create/upgrade transactions on-chain + +```bash +# (1) (use online machine) create buffer +solana program write-buffer + +# (2) (use online machine) upgrade buffer authority to offline signer +solana program set-buffer-authority --new-buffer-authority +``` + +- note, above we have to issue a separate `solana program set-buffer-authority` command (using online machine) to + change authority to be that of offline signer because we don't have access to offline signer private key to sign + up as buffer authority when running `solana program write-buffer` command using online machine (offline signer + private key is stored on offline machine), and `solana program set-buffer-authority` doesn't impose that requirement. + +(3) This is where you'd want to verify the program (or program buffer) uploaded to buffer indeed matches source code, +the most secure way to do it would be to compile program source code on a different online machine (not the same +online machine that deploys your program, because it could be compromised) and compare resulting `.so` file with +program data residing on-chain. See how you can [dump on-chain program into a file](deploy-a-program.md#dumping-a-program-to-a-file) +to verify it. + +After program buffer has been verified and is believed to contain the compiled result of intended source code - we are +ready to sign program deploy (to create brand-new program or upgrade existing one), fill in all required parameters +below ( values). + +```bash +# (4) (use offline machine) get a signature for your intent to CREATE program +solana program deploy --sign-only --fee-payer --program --upgrade-authority --buffer --blockhash --max-len --min-rent-balance +# or (4) (use offline machine) get a signature for your intent to UPGRADE program +solana program deploy --sign-only --upgrade --fee-payer --program --upgrade-authority --buffer --blockhash + +# (5) (use online machine) use this signature to build and broadcast create transactions on-chain +solana program deploy --fee-payer --program --upgrade-authority --buffer --blockhash --max-len --signer +# or (5) (use online machine) use this signature to build and broadcast upgrade transactions on-chain +solana program deploy --fee-payer --program --upgrade-authority --buffer --blockhash --signer +``` +Note: +- typically, the output of the previous command(s) will contain some values useful in subsequent commands, e.g. + `--buffer`, `--max-len`, `--min-rent-balance` (and you need to specify proper values for those in `--sign-only` mode + because on offline machine there is no way to query values online) +- you should pre-fill every value except for `blockhash` ahead of time, and once you are ready to act - you'll need to + look up fresh `blockhash` and paste it in quickly + do your signing, you have ~60 seconds before `blockhash` expires. + If you didn't make it in time - just get another fresh hash and repeat until you succeed, or consider using + [durable transaction nonces](../offline-signing/durable-nonce.md). diff --git a/remote-wallet/src/remote_keypair.rs b/remote-wallet/src/remote_keypair.rs index d37eefe2427175..9516e13d900bf4 100644 --- a/remote-wallet/src/remote_keypair.rs +++ b/remote-wallet/src/remote_keypair.rs @@ -14,6 +14,7 @@ use { }, }; +#[derive(Debug)] pub struct RemoteKeypair { pub wallet_type: RemoteWalletType, pub derivation_path: DerivationPath, diff --git a/sdk/src/signer/mod.rs b/sdk/src/signer/mod.rs index 710860e231981b..393642cfed5dec 100644 --- a/sdk/src/signer/mod.rs +++ b/sdk/src/signer/mod.rs @@ -12,6 +12,7 @@ use { itertools::Itertools, std::{ error, + fmt::Debug, fs::{self, File, OpenOptions}, io::{Read, Write}, ops::Deref, @@ -66,7 +67,7 @@ pub enum SignerError { /// The `Signer` trait declares operations that all digital signature providers /// must support. It is the primary interface by which signers are specified in /// `Transaction` signing interfaces -pub trait Signer { +pub trait Signer: Debug { /// Infallibly gets the implementor's public key. Returns the all-zeros /// `Pubkey` if the implementor has none. fn pubkey(&self) -> Pubkey { @@ -81,8 +82,12 @@ pub trait Signer { } /// Fallibly produces an Ed25519 signature over the provided `message` bytes. fn try_sign_message(&self, message: &[u8]) -> Result; - /// Whether the impelmentation requires user interaction to sign + /// Whether the implementation requires user interaction to sign fn is_interactive(&self) -> bool; + /// Whether the implementation is NullSigner + fn is_null_signer(&self) -> bool { + false // default reusable code + } } impl From for Box @@ -94,7 +99,8 @@ where } } -impl> Signer for Container { +/// This impl allows using Signer with types like Box/Rc/Arc. +impl + Debug> Signer for Container { #[inline] fn pubkey(&self) -> Pubkey { self.deref().pubkey() @@ -125,12 +131,6 @@ impl PartialEq for dyn Signer { impl Eq for dyn Signer {} -impl std::fmt::Debug for dyn Signer { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(fmt, "Signer: {:?}", self.pubkey()) - } -} - /// Removes duplicate signers while preserving order. O(n²) pub fn unique_signers(signers: Vec<&dyn Signer>) -> Vec<&dyn Signer> { signers.into_iter().unique_by(|s| s.pubkey()).collect() diff --git a/sdk/src/signer/null_signer.rs b/sdk/src/signer/null_signer.rs index 2e9508511832fd..e0642508e184e3 100644 --- a/sdk/src/signer/null_signer.rs +++ b/sdk/src/signer/null_signer.rs @@ -32,6 +32,10 @@ impl Signer for NullSigner { fn is_interactive(&self) -> bool { false } + + fn is_null_signer(&self) -> bool { + true + } } impl PartialEq for NullSigner diff --git a/sdk/src/signer/signers.rs b/sdk/src/signer/signers.rs index f4cfc7dc9618a0..f8f9ab06b622a2 100644 --- a/sdk/src/signer/signers.rs +++ b/sdk/src/signer/signers.rs @@ -15,6 +15,7 @@ pub trait Signers { fn sign_message(&self, message: &[u8]) -> Vec; fn try_sign_message(&self, message: &[u8]) -> Result, SignerError>; fn is_interactive(&self) -> bool; + fn is_null_signer(&self) -> bool; } macro_rules! default_keypairs_impl { @@ -48,6 +49,10 @@ macro_rules! default_keypairs_impl { fn is_interactive(&self) -> bool { self.iter().any(|s| s.is_interactive()) } + + fn is_null_signer(&self) -> bool { + self.iter().any(|s| s.is_null_signer()) + } }; } @@ -147,6 +152,7 @@ impl Signers for Vec<&T> { mod tests { use super::*; + #[derive(Debug)] struct Foo; impl Signer for Foo { fn try_pubkey(&self) -> Result { @@ -160,6 +166,7 @@ mod tests { } } + #[derive(Debug)] struct Bar; impl Signer for Bar { fn try_pubkey(&self) -> Result { diff --git a/transaction-dos/src/main.rs b/transaction-dos/src/main.rs index 5d69e9e291b6b5..399c675644e575 100644 --- a/transaction-dos/src/main.rs +++ b/transaction-dos/src/main.rs @@ -238,15 +238,19 @@ fn run_transactions_dos( config.signers = vec![payer_keypairs[0], &program_keypair]; config.command = CliCommand::Program(ProgramCliCommand::Deploy { program_location: Some(program_location), + fee_payer_signer_index: 0, program_signer_index: Some(1), - program_pubkey: None, buffer_signer_index: None, - buffer_pubkey: None, allow_excessive_balance: true, upgrade_authority_signer_index: 0, is_final: true, max_len: None, skip_fee_check: true, // skip_fee_check + sign_only: false, + upgrade: None, + dump_transaction_message: false, + blockhash_query: Default::default(), + min_rent_balance: None, }); process_command(&config).expect("deploy didn't pass");