From 1528959327e741fde1015a87223a3d349ddcb7a1 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Tue, 11 Feb 2020 00:23:54 -0700 Subject: [PATCH] CLI: Add fee-payer parame to stake-split subcommand (#8201) automerge --- cli/src/cli.rs | 4 ++ cli/src/stake.rs | 138 +++++++++++++++++++++++++++++++++++---------- cli/tests/stake.rs | 130 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 240 insertions(+), 32 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index cf0c771ade..606e8b9b39 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -313,6 +313,7 @@ pub enum CliCommand { split_stake_account: KeypairEq, seed: Option, lamports: u64, + fee_payer: Option, }, ShowStakeHistory { use_lamports_unit: bool, @@ -1612,6 +1613,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { split_stake_account, seed, lamports, + ref fee_payer, } => process_split_stake( &rpc_client, config, @@ -1625,6 +1627,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { split_stake_account, seed, *lamports, + fee_payer.as_ref(), ), CliCommand::ShowStakeAccount { pubkey: stake_account_pubkey, @@ -3058,6 +3061,7 @@ mod tests { split_stake_account: split_stake_account.into(), seed: None, lamports: 1234, + fee_payer: None, }; let signature = process_command(&config); assert_eq!(signature.unwrap(), SIGNATURE.to_string()); diff --git a/cli/src/stake.rs b/cli/src/stake.rs index bddeaee85d..42a829819f 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -296,6 +296,7 @@ impl StakeSubCommands for App<'_, '_> { .offline_args() .arg(nonce_arg()) .arg(nonce_authority_arg()) + .arg(fee_payer_arg()) ) .subcommand( SubCommand::with_name("withdraw-stake") @@ -484,6 +485,8 @@ pub fn parse_split_stake(matches: &ArgMatches<'_>) -> Result) -> Result, lamports: u64, + fee_payer: Option<&SigningAuthority>, ) -> ProcessResult { + let (fee_payer_pubkey, fee_payer) = fee_payer + .map(|f| (f.pubkey(), f.keypair())) + .unwrap_or((config.keypair.pubkey(), &config.keypair)); check_unique_pubkeys( - (&config.keypair.pubkey(), "cli keypair".to_string()), + (&fee_payer_pubkey, "fee-payer keypair".to_string()), ( &split_stake_account.pubkey(), "split_stake_account".to_string(), ), )?; check_unique_pubkeys( - (&config.keypair.pubkey(), "cli keypair".to_string()), + (&fee_payer_pubkey, "fee-payer keypair".to_string()), (&stake_account_pubkey, "stake_account".to_string()), )?; check_unique_pubkeys( @@ -880,30 +888,32 @@ pub fn process_split_stake( split_stake_account.pubkey() }; - if let Ok(stake_account) = rpc_client.get_account(&split_stake_account_address) { - let err_msg = if stake_account.owner == solana_stake_program::id() { - format!( - "Stake account {} already exists", - split_stake_account_address - ) - } else { - format!( - "Account {} already exists and is not a stake account", - split_stake_account_address - ) - }; - return Err(CliError::BadParameter(err_msg).into()); - } + if !sign_only { + if let Ok(stake_account) = rpc_client.get_account(&split_stake_account_address) { + let err_msg = if stake_account.owner == solana_stake_program::id() { + format!( + "Stake account {} already exists", + split_stake_account_address + ) + } else { + format!( + "Account {} already exists and is not a stake account", + split_stake_account_address + ) + }; + return Err(CliError::BadParameter(err_msg).into()); + } - let minimum_balance = - rpc_client.get_minimum_balance_for_rent_exemption(std::mem::size_of::())?; + let minimum_balance = + rpc_client.get_minimum_balance_for_rent_exemption(std::mem::size_of::())?; - if lamports < minimum_balance { - return Err(CliError::BadParameter(format!( - "need at least {} lamports for stake account to be rent exempt, provided lamports: {}", - minimum_balance, lamports - )) - .into()); + if lamports < minimum_balance { + return Err(CliError::BadParameter(format!( + "need at least {} lamports for stake account to be rent exempt, provided lamports: {}", + minimum_balance, lamports + )) + .into()); + } } let (recent_blockhash, fee_calculator) = @@ -934,9 +944,9 @@ pub fn process_split_stake( let mut tx = if let Some(nonce_account) = &nonce_account { Transaction::new_signed_with_nonce( ixs, - Some(&config.keypair.pubkey()), + Some(&fee_payer.pubkey()), &[ - &config.keypair, + fee_payer, nonce_authority, stake_authority, split_stake_account, @@ -948,8 +958,8 @@ pub fn process_split_stake( } else { Transaction::new_signed_with_payer( ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair, stake_authority, split_stake_account], + Some(&fee_payer.pubkey()), + &[fee_payer, stake_authority, split_stake_account], recent_blockhash, ) }; @@ -1220,7 +1230,7 @@ mod tests { use solana_sdk::{ fee_calculator::FeeCalculator, hash::Hash, - signature::{read_keypair_file, write_keypair}, + signature::{keypair_from_seed, read_keypair_file, write_keypair, KeypairUtil}, }; use tempfile::NamedTempFile; @@ -2195,12 +2205,78 @@ mod tests { blockhash_query: BlockhashQuery::default(), nonce_account: None, nonce_authority: None, - split_stake_account: split_stake_account_keypair.into(), + split_stake_account: read_keypair_file(&split_stake_account_keypair_file) + .unwrap() + .into(), seed: None, - lamports: 50 + lamports: 50, + fee_payer: None, }, require_keypair: true } ); + + // Split stake offline nonced submission + let nonce_account = Pubkey::new(&[1u8; 32]); + let nonce_account_string = nonce_account.to_string(); + let nonce_auth = keypair_from_seed(&[2u8; 32]).unwrap(); + let nonce_auth_pubkey = nonce_auth.pubkey(); + let nonce_auth_string = nonce_auth_pubkey.to_string(); + let nonce_sig = nonce_auth.sign_message(&[0u8]); + let nonce_signer = format!("{}={}", nonce_auth_pubkey, nonce_sig); + let stake_auth = keypair_from_seed(&[3u8; 32]).unwrap(); + let stake_auth_pubkey = stake_auth.pubkey(); + let stake_auth_string = stake_auth_pubkey.to_string(); + let stake_sig = stake_auth.sign_message(&[0u8]); + let stake_signer = format!("{}={}", stake_auth_pubkey, stake_sig); + let nonce_hash = Hash::new(&[4u8; 32]); + let nonce_hash_string = nonce_hash.to_string(); + + let test_split_stake_account = test_commands.clone().get_matches_from(vec![ + "test", + "split-stake", + &keypair_file, + &split_stake_account_keypair_file, + "50", + "lamports", + "--stake-authority", + &stake_auth_string, + "--blockhash", + &nonce_hash_string, + "--nonce", + &nonce_account_string, + "--nonce-authority", + &nonce_auth_string, + "--fee-payer", + &nonce_auth_string, // Arbitrary choice of fee-payer + "--signer", + &nonce_signer, + "--signer", + &stake_signer, + ]); + assert_eq!( + parse_command(&test_split_stake_account).unwrap(), + CliCommandInfo { + command: CliCommand::SplitStake { + stake_account_pubkey: stake_account_keypair.pubkey(), + stake_authority: Some(stake_auth_pubkey.into()), + sign_only: false, + signers: Some(vec![ + (nonce_auth_pubkey, nonce_sig), + (stake_auth_pubkey, stake_sig) + ]), + blockhash_query: BlockhashQuery::FeeCalculator(nonce_hash), + nonce_account: Some(nonce_account.into()), + nonce_authority: Some(nonce_auth_pubkey.into()), + split_stake_account: read_keypair_file(&split_stake_account_keypair_file) + .unwrap() + .into(), + seed: None, + lamports: 50, + fee_payer: Some(nonce_auth_pubkey.into()), + }, + require_keypair: false, + } + ); } } diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index a5afc16752..ece6c5b6a1 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -9,7 +9,7 @@ use solana_sdk::{ fee_calculator::FeeCalculator, nonce_state::NonceState, pubkey::Pubkey, - signature::{read_keypair_file, write_keypair, Keypair, KeypairUtil}, + signature::{keypair_from_seed, read_keypair_file, write_keypair, Keypair, KeypairUtil}, system_instruction::create_address_with_seed, }; use solana_stake_program::stake_state::{Lockup, StakeAuthorize, StakeState}; @@ -844,3 +844,131 @@ fn test_stake_authorize_with_fee_payer() { server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); } + +#[test] +fn test_stake_split() { + solana_logger::setup(); + + let (server, leader_data, alice, ledger_path, _voter) = new_validator_for_tests_ex(1, 42_000); + let (sender, receiver) = channel(); + run_local_faucet(alice, sender, None); + let faucet_addr = receiver.recv().unwrap(); + + let rpc_client = RpcClient::new_socket(leader_data.rpc); + + let mut config = CliConfig::default(); + config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + + let mut config_offline = CliConfig::default(); + let offline_pubkey = config_offline.keypair.pubkey(); + let (_offline_keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&config_offline.keypair, tmp_file.as_file_mut()).unwrap(); + // Verify we're offline + config_offline.command = CliCommand::ClusterVersion; + process_command(&config_offline).unwrap_err(); + + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &config.keypair.pubkey(), 500_000) + .unwrap(); + check_balance(500_000, &rpc_client, &config.keypair.pubkey()); + + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_pubkey, 100_000).unwrap(); + check_balance(100_000, &rpc_client, &offline_pubkey); + + // Create stake account, identity is authority + let minimum_stake_balance = rpc_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::()) + .unwrap(); + println!("stake min: {}", minimum_stake_balance); + let stake_keypair = keypair_from_seed(&[0u8; 32]).unwrap(); + let stake_account_pubkey = stake_keypair.pubkey(); + let (stake_keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&stake_keypair, tmp_file.as_file_mut()).unwrap(); + config.command = CliCommand::CreateStakeAccount { + stake_account: read_keypair_file(&stake_keypair_file).unwrap().into(), + seed: None, + staker: Some(offline_pubkey), + withdrawer: Some(offline_pubkey), + lockup: Lockup::default(), + lamports: 10 * minimum_stake_balance, + }; + process_command(&config).unwrap(); + check_balance( + 10 * minimum_stake_balance, + &rpc_client, + &stake_account_pubkey, + ); + + // Create nonce account + let minimum_nonce_balance = rpc_client + .get_minimum_balance_for_rent_exemption(NonceState::size()) + .unwrap(); + println!("nonce min: {}", minimum_nonce_balance); + let nonce_account = keypair_from_seed(&[1u8; 32]).unwrap(); + let nonce_account_pubkey = nonce_account.pubkey(); + let (nonce_keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&nonce_account, tmp_file.as_file_mut()).unwrap(); + config.command = CliCommand::CreateNonceAccount { + nonce_account: read_keypair_file(&nonce_keypair_file).unwrap().into(), + seed: None, + nonce_authority: Some(offline_pubkey), + lamports: minimum_nonce_balance, + }; + process_command(&config).unwrap(); + check_balance(minimum_nonce_balance, &rpc_client, &nonce_account_pubkey); + + // Fetch nonce hash + let account = rpc_client.get_account(&nonce_account_pubkey).unwrap(); + let nonce_state: NonceState = account.state().unwrap(); + let nonce_hash = match nonce_state { + NonceState::Initialized(_meta, hash) => hash, + _ => panic!("Nonce is not initialized"), + }; + + // Nonced offline split + let split_account = keypair_from_seed(&[2u8; 32]).unwrap(); + let (split_keypair_file, mut tmp_file) = make_tmp_file(); + write_keypair(&split_account, tmp_file.as_file_mut()).unwrap(); + check_balance(0, &rpc_client, &split_account.pubkey()); + config_offline.command = CliCommand::SplitStake { + stake_account_pubkey: stake_account_pubkey, + stake_authority: None, + sign_only: true, + signers: None, + blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), + nonce_account: Some(nonce_account_pubkey.into()), + nonce_authority: None, + split_stake_account: read_keypair_file(&split_keypair_file).unwrap().into(), + seed: None, + lamports: 2 * minimum_stake_balance, + fee_payer: None, + }; + let sig_response = process_command(&config_offline).unwrap(); + let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + config.command = CliCommand::SplitStake { + stake_account_pubkey: stake_account_pubkey, + stake_authority: Some(offline_pubkey.into()), + sign_only: false, + signers: Some(signers), + blockhash_query: BlockhashQuery::FeeCalculator(blockhash), + nonce_account: Some(nonce_account_pubkey.into()), + nonce_authority: Some(offline_pubkey.into()), + split_stake_account: read_keypair_file(&split_keypair_file).unwrap().into(), + seed: None, + lamports: 2 * minimum_stake_balance, + fee_payer: Some(offline_pubkey.into()), + }; + process_command(&config).unwrap(); + check_balance( + 8 * minimum_stake_balance, + &rpc_client, + &stake_account_pubkey, + ); + check_balance( + 2 * minimum_stake_balance, + &rpc_client, + &split_account.pubkey(), + ); + + server.close().unwrap(); + remove_dir_all(ledger_path).unwrap(); +}