diff --git a/cli/src/program.rs b/cli/src/program.rs index 5640a89738eedf..26b7db382e4764 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -54,7 +54,7 @@ use { account::Account, account_utils::StateMut, bpf_loader, bpf_loader_deprecated, - bpf_loader_upgradeable::{self, UpgradeableLoaderState}, + bpf_loader_upgradeable::{self, get_program_data_address, UpgradeableLoaderState}, compute_budget, feature_set::FeatureSet, instruction::{Instruction, InstructionError}, @@ -99,6 +99,7 @@ pub enum ProgramCliCommand { skip_fee_check: bool, compute_unit_price: Option, max_sign_attempts: usize, + no_extend: bool, use_rpc: bool, }, Upgrade { @@ -273,7 +274,13 @@ impl ProgramSubCommands for App<'_, '_> { .arg(Arg::with_name("use_rpc").long("use-rpc").help( "Send write transactions to the configured RPC instead of validator TPUs", )) - .arg(compute_unit_price_arg()), + .arg(compute_unit_price_arg()) + .arg( + Arg::with_name("no_extend") + .long("no-extend") + .takes_value(false) + .help("Don't automatically extend the program's data account size"), + ), ) .subcommand( SubCommand::with_name("upgrade") @@ -665,6 +672,8 @@ pub fn parse_program_subcommand( let compute_unit_price = value_of(matches, "compute_unit_price"); let max_sign_attempts = value_of(matches, "max_sign_attempts").unwrap(); + let no_extend = matches.is_present("no_extend"); + CliCommandInfo { command: CliCommand::Program(ProgramCliCommand::Deploy { program_location, @@ -683,6 +692,7 @@ pub fn parse_program_subcommand( compute_unit_price, max_sign_attempts, use_rpc: matches.is_present("use_rpc"), + no_extend, }), signers: signer_info.signers, } @@ -970,6 +980,7 @@ pub fn process_program_subcommand( skip_fee_check, compute_unit_price, max_sign_attempts, + no_extend, use_rpc, } => process_program_deploy( rpc_client, @@ -987,6 +998,7 @@ pub fn process_program_subcommand( *skip_fee_check, *compute_unit_price, *max_sign_attempts, + *no_extend, *use_rpc, ), ProgramCliCommand::Upgrade { @@ -1165,6 +1177,7 @@ fn process_program_deploy( skip_fee_check: bool, compute_unit_price: Option, max_sign_attempts: usize, + no_extend: bool, use_rpc: bool, ) -> ProcessResult { let fee_payer_signer = config.signers[fee_payer_signer_index]; @@ -1350,6 +1363,7 @@ fn process_program_deploy( skip_fee_check, compute_unit_price, max_sign_attempts, + no_extend, use_rpc, ) }; @@ -2490,6 +2504,7 @@ fn do_process_program_upgrade( skip_fee_check: bool, compute_unit_price: Option, max_sign_attempts: usize, + no_extend: bool, use_rpc: bool, ) -> ProcessResult { let blockhash = rpc_client.get_latest_blockhash()?; @@ -2498,7 +2513,7 @@ fn do_process_program_upgrade( buffer_signer { // Check Buffer account to see if partial initialization has occurred - let (initial_instructions, balance_needed, buffer_program_data) = + let (mut initial_instructions, balance_needed, buffer_program_data) = if let Some(mut account) = buffer_account { let (ixs, balance_needed) = complete_partial_program_init( &fee_payer_signer.pubkey(), @@ -2526,6 +2541,35 @@ fn do_process_program_upgrade( ) }; + if !no_extend { + // Attempt to look up the existing program's size, and automatically + // add an extend instruction if the program data account is too small. + let program_data_address = get_program_data_address(program_id); + if let Some(program_data_account) = rpc_client + .get_account_with_commitment(&program_data_address, config.commitment)? + .value + { + let program_len = UpgradeableLoaderState::size_of_programdata(program_len); + let account_data_len = program_data_account.data.len(); + if program_len > account_data_len { + let additional_bytes = program_len.saturating_sub(account_data_len); + let additional_bytes: u32 = additional_bytes.try_into().map_err(|_| { + format!( + "Cannot auto-extend Program Data Account space due to size limit \ + please extend it manually with command `solana program extend {} \ + `. Additional bytes required: {}", + program_id, additional_bytes + ) + })?; + initial_instructions.push(bpf_loader_upgradeable::extend_program( + program_id, + Some(&fee_payer_signer.pubkey()), + additional_bytes, + )); + } + } + } + let initial_message = if !initial_instructions.is_empty() { Some(Message::new_with_blockhash( &initial_instructions @@ -2959,6 +3003,7 @@ mod tests { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], @@ -2990,6 +3035,7 @@ mod tests { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], @@ -3023,6 +3069,7 @@ mod tests { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }), signers: vec![ @@ -3058,6 +3105,7 @@ mod tests { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], @@ -3092,6 +3140,7 @@ mod tests { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }), signers: vec![ @@ -3129,6 +3178,7 @@ mod tests { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }), signers: vec![ @@ -3162,6 +3212,7 @@ mod tests { allow_excessive_balance: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], @@ -3193,6 +3244,7 @@ mod tests { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 1, + no_extend: false, use_rpc: false, }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], @@ -3223,6 +3275,7 @@ mod tests { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: true, }), signers: vec![Box::new(read_keypair_file(&keypair_file).unwrap())], @@ -3966,6 +4019,7 @@ mod tests { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }), signers: vec![&default_keypair], diff --git a/cli/tests/program.rs b/cli/tests/program.rs index 5e2098de9c455a..2215f84a453af9 100644 --- a/cli/tests/program.rs +++ b/cli/tests/program.rs @@ -19,6 +19,7 @@ use { solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_nonce_utils::blockhash_query::BlockhashQuery, solana_sdk::{ + account::ReadableAccount, account_utils::StateMut, borsh1::try_from_slice_unchecked, bpf_loader_upgradeable::{self, UpgradeableLoaderState}, @@ -99,6 +100,7 @@ fn test_cli_program_deploy_non_upgradeable() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); config.output_format = OutputFormat::JsonCompact; @@ -148,6 +150,7 @@ fn test_cli_program_deploy_non_upgradeable() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); process_command(&config).unwrap(); @@ -206,6 +209,7 @@ fn test_cli_program_deploy_non_upgradeable() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); let err = process_command(&config).unwrap_err(); @@ -232,6 +236,7 @@ fn test_cli_program_deploy_non_upgradeable() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); process_command(&config).unwrap_err(); @@ -296,6 +301,7 @@ fn test_cli_program_deploy_no_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); config.output_format = OutputFormat::JsonCompact; @@ -326,6 +332,7 @@ fn test_cli_program_deploy_no_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); process_command(&config).unwrap_err(); @@ -391,6 +398,7 @@ fn test_cli_program_deploy_with_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); config.output_format = OutputFormat::JsonCompact; @@ -443,6 +451,7 @@ fn test_cli_program_deploy_with_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); let response = process_command(&config); @@ -489,6 +498,7 @@ fn test_cli_program_deploy_with_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); process_command(&config).unwrap(); @@ -567,6 +577,7 @@ fn test_cli_program_deploy_with_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); process_command(&config).unwrap(); @@ -649,6 +660,7 @@ fn test_cli_program_deploy_with_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); process_command(&config).unwrap_err(); @@ -669,6 +681,7 @@ fn test_cli_program_deploy_with_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); let response = process_command(&config); @@ -716,6 +729,154 @@ fn test_cli_program_deploy_with_authority() { assert_eq!("none", authority_pubkey_str); } +#[test] +fn test_cli_program_upgrade_auto_extend() { + 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 mut file = File::open(noop_path.to_str().unwrap()).unwrap(); + let mut program_data = Vec::new(); + file.read_to_end(&mut program_data).unwrap(); + + let mut file = File::open(noop_large_path.to_str().unwrap()).unwrap(); + let mut program_data_large = Vec::new(); + file.read_to_end(&mut program_data_large).unwrap(); + + // Use the larger program to calculate rent. + let max_len = program_data_large.len(); + let minimum_balance_for_programdata = rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_programdata( + max_len, + )) + .unwrap(); + let minimum_balance_for_program = rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program()) + .unwrap(); + let upgrade_authority = Keypair::new(); + + let mut config = CliConfig::recent_for_tests(); + let keypair = Keypair::new(); + config.json_rpc_url = test_validator.rpc_url(); + config.signers = vec![&keypair]; + config.command = CliCommand::Airdrop { + pubkey: None, + lamports: 100 * minimum_balance_for_programdata + minimum_balance_for_program, + }; + process_command(&config).unwrap(); + + // Deploy the first, smaller program. + let program_keypair = Keypair::new(); + config.signers = vec![&keypair, &upgrade_authority, &program_keypair]; + config.command = CliCommand::Program(ProgramCliCommand::Deploy { + program_location: Some(noop_path.to_str().unwrap().to_string()), + fee_payer_signer_index: 0, + program_signer_index: Some(2), + program_pubkey: Some(program_keypair.pubkey()), + buffer_signer_index: None, + buffer_pubkey: None, + allow_excessive_balance: false, + upgrade_authority_signer_index: 1, + is_final: false, + max_len: None, + skip_fee_check: false, + compute_unit_price: None, + max_sign_attempts: 5, + no_extend: false, + use_rpc: false, + }); + config.output_format = OutputFormat::JsonCompact; + process_command(&config).unwrap(); + + // Attempt to upgrade the program with a larger program, but with the + // --no-extend flag. + config.signers = vec![&keypair, &upgrade_authority]; + 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: Some(program_keypair.pubkey()), + 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, + compute_unit_price: None, + max_sign_attempts: 5, + no_extend: true, // --no-extend (true) + use_rpc: false, + }); + process_command(&config).unwrap_err(); + + // Attempt to upgrade the program with a larger program, this time without + // the --no-extend flag. This should automatically extend the program data. + config.signers = vec![&keypair, &upgrade_authority]; + 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: Some(program_keypair.pubkey()), + 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, + compute_unit_price: None, + max_sign_attempts: 5, + no_extend: false, // --no-extend (false) + use_rpc: false, + }); + let response = process_command(&config); + let json: Value = serde_json::from_str(&response.unwrap()).unwrap(); + let program_pubkey_str = json + .as_object() + .unwrap() + .get("programId") + .unwrap() + .as_str() + .unwrap(); + let program_pubkey = Pubkey::from_str(program_pubkey_str).unwrap(); + let (programdata_pubkey, _) = + Pubkey::find_program_address(&[program_pubkey.as_ref()], &bpf_loader_upgradeable::id()); + let programdata_account = rpc_client.get_account(&programdata_pubkey).unwrap(); + if let UpgradeableLoaderState::ProgramData { + slot: _, + upgrade_authority_address, + } = programdata_account.state().unwrap() + { + assert_eq!(upgrade_authority_address, None); + } else { + panic!("not a ProgramData account"); + } + assert_eq!( + programdata_account.data().len(), + UpgradeableLoaderState::size_of_programdata(program_data_large.len()), + ); +} + #[test] fn test_cli_program_close_program() { solana_logger::setup(); @@ -776,6 +937,7 @@ fn test_cli_program_close_program() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); config.output_format = OutputFormat::JsonCompact; @@ -889,6 +1051,7 @@ fn test_cli_program_extend_program() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); config.output_format = OutputFormat::JsonCompact; @@ -939,6 +1102,7 @@ fn test_cli_program_extend_program() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); process_command(&config).unwrap_err(); @@ -974,6 +1138,7 @@ fn test_cli_program_extend_program() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); process_command(&config).unwrap(); @@ -1329,6 +1494,7 @@ fn test_cli_program_write_buffer() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); config.output_format = OutputFormat::JsonCompact; @@ -1459,6 +1625,7 @@ fn test_cli_program_set_buffer_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); config.output_format = OutputFormat::JsonCompact; @@ -1507,6 +1674,7 @@ fn test_cli_program_set_buffer_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); config.output_format = OutputFormat::JsonCompact; @@ -1593,6 +1761,7 @@ fn test_cli_program_mismatch_buffer_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); process_command(&config).unwrap_err(); @@ -1613,6 +1782,7 @@ fn test_cli_program_mismatch_buffer_authority() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); process_command(&config).unwrap(); @@ -1699,6 +1869,7 @@ fn test_cli_program_deploy_with_offline_signing(use_offline_signer_as_fee_payer: skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); config.output_format = OutputFormat::JsonCompact; @@ -1933,6 +2104,7 @@ fn test_cli_program_show() { skip_fee_check: false, compute_unit_price: None, max_sign_attempts: 5, + no_extend: false, use_rpc: false, }); config.output_format = OutputFormat::JsonCompact; @@ -2210,6 +2382,7 @@ fn test_cli_program_deploy_with_args(compute_unit_price: Option, use_rpc: b skip_fee_check: false, compute_unit_price, max_sign_attempts: 5, + no_extend: false, use_rpc, }); config.output_format = OutputFormat::JsonCompact; diff --git a/transaction-dos/src/main.rs b/transaction-dos/src/main.rs index 1e45a1fa5ccda4..8cd02173d62591 100644 --- a/transaction-dos/src/main.rs +++ b/transaction-dos/src/main.rs @@ -251,6 +251,7 @@ fn run_transactions_dos( max_sign_attempts: 5, use_rpc: false, skip_fee_check: true, // skip_fee_check + no_extend: false, }); process_command(&config).expect("deploy didn't pass");