|
|
@@ -1,4 +1,6 @@
|
|
|
#![allow(clippy::arithmetic_side_effects)]
|
|
|
+// REMOVE once https://github.com/rust-lang/rust-clippy/issues/11153 is fixed
|
|
|
+#![allow(clippy::items_after_test_module)]
|
|
|
|
|
|
use {
|
|
|
serde_json::Value,
|
|
|
@@ -7,19 +9,27 @@ use {
|
|
|
program::{ProgramCliCommand, CLOSE_PROGRAM_WARNING},
|
|
|
test_utils::wait_n_slots,
|
|
|
},
|
|
|
- solana_cli_output::OutputFormat,
|
|
|
+ solana_cli_output::{parse_sign_only_reply_string, 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},
|
|
|
commitment_config::CommitmentConfig,
|
|
|
pubkey::Pubkey,
|
|
|
- signature::{Keypair, Signer},
|
|
|
+ signature::{Keypair, NullSigner, Signer},
|
|
|
},
|
|
|
solana_streamer::socket::SocketAddrSpace,
|
|
|
solana_test_validator::TestValidator,
|
|
|
- std::{env, fs::File, io::Read, path::PathBuf, str::FromStr},
|
|
|
+ std::{
|
|
|
+ env,
|
|
|
+ fs::File,
|
|
|
+ io::Read,
|
|
|
+ path::{Path, PathBuf},
|
|
|
+ str::FromStr,
|
|
|
+ },
|
|
|
+ test_case::test_case,
|
|
|
};
|
|
|
|
|
|
#[test]
|
|
|
@@ -1411,6 +1421,198 @@ fn test_cli_program_mismatch_buffer_authority() {
|
|
|
process_command(&config).unwrap();
|
|
|
}
|
|
|
|
|
|
+// 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 for most common choices.
|
|
|
+#[test_case(true; "offline signer will be fee payer")]
|
|
|
+#[test_case(false; "online signer will be fee payer")]
|
|
|
+fn test_cli_program_deploy_with_offline_signing(use_offline_signer_as_fee_payer: bool) {
|
|
|
+ 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 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();
|
|
|
+ // Typically, keypair for program signer should be different from online signer or
|
|
|
+ // offline signer keypairs.
|
|
|
+ let program_signer = Keypair::new();
|
|
|
+
|
|
|
+ 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();
|
|
|
+
|
|
|
+ // Deploy upgradeable program with authority set to offline signer
|
|
|
+ config.signers = vec![&online_signer, &offline_signer, &program_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: Some(2),
|
|
|
+ program_pubkey: Some(program_signer.pubkey()),
|
|
|
+ buffer_signer_index: None,
|
|
|
+ buffer_pubkey: None,
|
|
|
+ allow_excessive_balance: false,
|
|
|
+ upgrade_authority_signer_index: 1, // must be offline signer for security reasons
|
|
|
+ is_final: false,
|
|
|
+ max_len: Some(max_program_data_len), // allows for larger program size with future upgrades
|
|
|
+ skip_fee_check: false,
|
|
|
+ });
|
|
|
+ config.output_format = OutputFormat::JsonCompact;
|
|
|
+ process_command(&config).unwrap();
|
|
|
+
|
|
|
+ // Prepare buffer to upgrade deployed program to a larger program
|
|
|
+ create_buffer_with_offline_authority(
|
|
|
+ &rpc_client,
|
|
|
+ &noop_large_path,
|
|
|
+ &mut config,
|
|
|
+ &online_signer,
|
|
|
+ &offline_signer,
|
|
|
+ &buffer_signer,
|
|
|
+ );
|
|
|
+
|
|
|
+ // Offline sign-only with signature over "wrong" message (with different buffer)
|
|
|
+ config.signers = vec![&offline_signer];
|
|
|
+ let fee_payer_signer_index = if use_offline_signer_as_fee_payer {
|
|
|
+ 0 // offline signer
|
|
|
+ } else {
|
|
|
+ config.signers.push(&online_signer_identity); // can't (and won't) provide signature in --sign-only mode
|
|
|
+ 1 // online signer
|
|
|
+ };
|
|
|
+ config.command = CliCommand::Program(ProgramCliCommand::Upgrade {
|
|
|
+ fee_payer_signer_index,
|
|
|
+ program_pubkey: program_signer.pubkey(),
|
|
|
+ buffer_pubkey: program_signer.pubkey(), // will ensure offline signature applies to wrong(different) message
|
|
|
+ upgrade_authority_signer_index: 0,
|
|
|
+ sign_only: true,
|
|
|
+ dump_transaction_message: false,
|
|
|
+ blockhash_query: BlockhashQuery::new(Some(blockhash), true, None),
|
|
|
+ });
|
|
|
+ config.output_format = OutputFormat::JsonCompact;
|
|
|
+ let sig_response = process_command(&config).unwrap();
|
|
|
+ let sign_only = parse_sign_only_reply_string(&sig_response);
|
|
|
+ let offline_pre_signer = sign_only.presigner_of(&offline_signer.pubkey()).unwrap();
|
|
|
+ // Attempt to deploy from buffer using signature over wrong(different) message (should fail)
|
|
|
+ config.signers = vec![&offline_pre_signer, &program_signer];
|
|
|
+ let fee_payer_signer_index = if use_offline_signer_as_fee_payer {
|
|
|
+ 0 // offline signer
|
|
|
+ } else {
|
|
|
+ config.signers.push(&online_signer); // can provide signature when not in --sign-only mode
|
|
|
+ 2 // online signer
|
|
|
+ };
|
|
|
+ config.command = CliCommand::Program(ProgramCliCommand::Upgrade {
|
|
|
+ fee_payer_signer_index,
|
|
|
+ program_pubkey: program_signer.pubkey(),
|
|
|
+ buffer_pubkey: buffer_signer.pubkey(),
|
|
|
+ upgrade_authority_signer_index: 0,
|
|
|
+ sign_only: false,
|
|
|
+ dump_transaction_message: false,
|
|
|
+ blockhash_query: BlockhashQuery::new(Some(blockhash), true, 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];
|
|
|
+ let fee_payer_signer_index = if use_offline_signer_as_fee_payer {
|
|
|
+ 0 // offline signer
|
|
|
+ } else {
|
|
|
+ config.signers.push(&online_signer_identity); // can't (and won't) provide signature in --sign-only mode
|
|
|
+ 1 // online signer
|
|
|
+ };
|
|
|
+ config.command = CliCommand::Program(ProgramCliCommand::Upgrade {
|
|
|
+ fee_payer_signer_index,
|
|
|
+ program_pubkey: program_signer.pubkey(),
|
|
|
+ buffer_pubkey: buffer_signer.pubkey(),
|
|
|
+ upgrade_authority_signer_index: 0,
|
|
|
+ sign_only: true,
|
|
|
+ dump_transaction_message: false,
|
|
|
+ blockhash_query: BlockhashQuery::new(Some(blockhash), true, None),
|
|
|
+ });
|
|
|
+ config.output_format = OutputFormat::JsonCompact;
|
|
|
+ let sig_response = process_command(&config).unwrap();
|
|
|
+ let sign_only = parse_sign_only_reply_string(&sig_response);
|
|
|
+ let offline_pre_signer = sign_only.presigner_of(&offline_signer.pubkey()).unwrap();
|
|
|
+ // Attempt to deploy from buffer using signature over correct message (should succeed)
|
|
|
+ config.signers = vec![&offline_pre_signer, &program_signer];
|
|
|
+ let fee_payer_signer_index = if use_offline_signer_as_fee_payer {
|
|
|
+ 0 // offline signer
|
|
|
+ } else {
|
|
|
+ config.signers.push(&online_signer); // can provide signature when not in --sign-only mode
|
|
|
+ 2 // online signer
|
|
|
+ };
|
|
|
+ config.command = CliCommand::Program(ProgramCliCommand::Upgrade {
|
|
|
+ fee_payer_signer_index,
|
|
|
+ program_pubkey: program_signer.pubkey(),
|
|
|
+ buffer_pubkey: buffer_signer.pubkey(),
|
|
|
+ upgrade_authority_signer_index: 0,
|
|
|
+ sign_only: false,
|
|
|
+ dump_transaction_message: false,
|
|
|
+ blockhash_query: BlockhashQuery::new(Some(blockhash), true, None),
|
|
|
+ });
|
|
|
+ config.output_format = OutputFormat::JsonCompact;
|
|
|
+ process_command(&config).unwrap();
|
|
|
+ let (programdata_pubkey, _) = Pubkey::find_program_address(
|
|
|
+ &[program_signer.pubkey().as_ref()],
|
|
|
+ &bpf_loader_upgradeable::id(),
|
|
|
+ );
|
|
|
+ let programdata_account = rpc_client.get_account(&programdata_pubkey).unwrap();
|
|
|
+ assert_eq!(
|
|
|
+ programdata_account.lamports,
|
|
|
+ minimum_balance_for_large_buffer
|
|
|
+ );
|
|
|
+ assert_eq!(programdata_account.owner, bpf_loader_upgradeable::id());
|
|
|
+ assert!(!programdata_account.executable);
|
|
|
+ assert_eq!(
|
|
|
+ programdata_account.data[UpgradeableLoaderState::size_of_programdata_metadata()..],
|
|
|
+ large_program_data[..]
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
#[test]
|
|
|
fn test_cli_program_show() {
|
|
|
solana_logger::setup();
|
|
|
@@ -1678,3 +1880,47 @@ fn test_cli_program_dump() {
|
|
|
assert_eq!(program_data[i], out_data[i]);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+fn create_buffer_with_offline_authority<'a>(
|
|
|
+ rpc_client: &RpcClient,
|
|
|
+ program_path: &Path,
|
|
|
+ config: &mut CliConfig<'a>,
|
|
|
+ online_signer: &'a Keypair,
|
|
|
+ offline_signer: &'a Keypair,
|
|
|
+ buffer_signer: &'a Keypair,
|
|
|
+) {
|
|
|
+ // 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];
|
|
|
+ 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;
|
|
|
+ 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(offline_signer.pubkey()));
|
|
|
+ } else {
|
|
|
+ panic!("not a buffer account");
|
|
|
+ }
|
|
|
+}
|