From 6309c9769716ed5fd30ecc11600dae5d1569f15e Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Wed, 12 Feb 2020 16:36:29 -0700 Subject: [PATCH] Add CliCommand::StakeSetLockup (#8248) automerge --- cli/src/cli.rs | 36 ++++++- cli/src/stake.rs | 160 ++++++++++++++++++++++++++++++- cli/tests/stake.rs | 229 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 417 insertions(+), 8 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 5039ffb555..4968fb1a8a 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -334,6 +334,17 @@ pub enum CliCommand { nonce_authority: Option, fee_payer: Option, }, + StakeSetLockup { + stake_account_pubkey: Pubkey, + lockup: Lockup, + custodian: Option, + sign_only: bool, + signers: Option>, + blockhash_query: BlockhashQuery, + nonce_account: Option, + nonce_authority: Option, + fee_payer: Option, + }, WithdrawStake { stake_account_pubkey: Pubkey, destination_account_pubkey: Pubkey, @@ -565,6 +576,7 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result { parse_stake_authorize(matches, StakeAuthorize::Withdrawer) } + ("stake-set-lockup", Some(matches)) => parse_stake_set_lockup(matches), ("stake-account", Some(matches)) => parse_show_stake_account(matches), ("stake-history", Some(matches)) => parse_show_stake_history(matches), // Storage Commands @@ -1665,7 +1677,29 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { nonce_authority.as_ref(), fee_payer.as_ref(), ), - + CliCommand::StakeSetLockup { + stake_account_pubkey, + mut lockup, + ref custodian, + sign_only, + ref signers, + blockhash_query, + nonce_account, + ref nonce_authority, + ref fee_payer, + } => process_stake_set_lockup( + &rpc_client, + config, + &stake_account_pubkey, + &mut lockup, + custodian.as_ref(), + *sign_only, + signers, + blockhash_query, + *nonce_account, + nonce_authority.as_ref(), + fee_payer.as_ref(), + ), CliCommand::WithdrawStake { stake_account_pubkey, destination_account_pubkey, diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 42a829819f..e9aab398b7 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -47,7 +47,7 @@ fn stake_authority_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name(STAKE_AUTHORITY_ARG.name) .long(STAKE_AUTHORITY_ARG.long) .takes_value(true) - .value_name("KEYPAIR of PUBKEY") + .value_name("KEYPAIR or PUBKEY") .validator(is_pubkey_or_keypair_or_ask_keyword) .help(STAKE_AUTHORITY_ARG.help) } @@ -99,7 +99,7 @@ impl StakeSubCommands for App<'_, '_> { .arg( Arg::with_name("custodian") .long("custodian") - .value_name("PUBKEY") + .value_name("KEYPAIR or PUBKEY") .takes_value(true) .validator(is_pubkey_or_keypair) .help("Identity of the custodian (can withdraw before lockup expires)") @@ -337,7 +337,55 @@ impl StakeSubCommands for App<'_, '_> { .help("Specify unit to use for request") ) .arg(withdraw_authority_arg()) - ) + ) + .subcommand( + SubCommand::with_name("stake-set-lockup") + .about("Set Lockup for the stake account") + .arg( + Arg::with_name("stake_account_pubkey") + .index(1) + .value_name("STAKE ACCOUNT") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Stake account for which to set Lockup") + ) + .arg( + Arg::with_name("lockup_epoch") + .long("lockup-epoch") + .value_name("EPOCH") + .takes_value(true) + .help("The epoch height at which this account will be available for withdrawal") + ) + .arg( + Arg::with_name("lockup_date") + .long("lockup-date") + .value_name("RFC3339 DATE TIME") + .validator(is_rfc3339_datetime) + .takes_value(true) + .help("The date and time at which this account will be available for withdrawal") + ) + .arg( + Arg::with_name("new_custodian") + .long("new-custodian") + .value_name("KEYPAIR or PUBKEY") + .takes_value(true) + .validator(is_pubkey_or_keypair) + .help("Identity of the new lockup custodian (can withdraw before lockup expires)") + ) + .arg( + Arg::with_name("custodian") + .long("custodian") + .takes_value(true) + .value_name("KEYPAIR or PUBKEY") + .validator(is_pubkey_or_keypair_or_ask_keyword) + .help("Public key of signing custodian (defaults to cli config pubkey)") + ) + .offline_args() + .arg(nonce_arg()) + .arg(nonce_authority_arg()) + .arg(fee_payer_arg()) + ) .subcommand( SubCommand::with_name("stake-account") .about("Show the contents of a stake account") @@ -553,6 +601,44 @@ pub fn parse_stake_withdraw_stake(matches: &ArgMatches<'_>) -> Result) -> Result { + let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); + let epoch = value_of(&matches, "lockup_epoch").unwrap_or(0); + let unix_timestamp = unix_timestamp_from_rfc3339_datetime(&matches, "lockup_date").unwrap_or(0); + let new_custodian = pubkey_of(matches, "new_custodian").unwrap_or_default(); + + let sign_only = matches.is_present(SIGN_ONLY_ARG.name); + let signers = pubkeys_sigs_of(&matches, SIGNER_ARG.name); + let blockhash_query = BlockhashQuery::new_from_matches(matches); + let require_keypair = signers.is_none(); + let nonce_account = pubkey_of(&matches, NONCE_ARG.name); + + let custodian = SigningAuthority::new_from_matches(&matches, "custodian", signers.as_deref())?; + let nonce_authority = + SigningAuthority::new_from_matches(&matches, NONCE_AUTHORITY_ARG.name, signers.as_deref())?; + let fee_payer = + SigningAuthority::new_from_matches(&matches, FEE_PAYER_ARG.name, signers.as_deref())?; + + Ok(CliCommandInfo { + command: CliCommand::StakeSetLockup { + stake_account_pubkey, + lockup: Lockup { + custodian: new_custodian, + epoch, + unix_timestamp, + }, + custodian, + sign_only, + signers, + blockhash_query, + nonce_account, + nonce_authority, + fee_payer, + }, + require_keypair, + }) +} + pub fn parse_show_stake_account(matches: &ArgMatches<'_>) -> Result { let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap(); let use_lamports_unit = matches.is_present("lamports"); @@ -984,6 +1070,74 @@ pub fn process_split_stake( } } +#[allow(clippy::too_many_arguments)] +pub fn process_stake_set_lockup( + rpc_client: &RpcClient, + config: &CliConfig, + stake_account_pubkey: &Pubkey, + lockup: &mut Lockup, + custodian: Option<&SigningAuthority>, + sign_only: bool, + signers: &Option>, + blockhash_query: &BlockhashQuery, + nonce_account: Option, + nonce_authority: Option<&SigningAuthority>, + fee_payer: Option<&SigningAuthority>, +) -> ProcessResult { + let (recent_blockhash, fee_calculator) = + blockhash_query.get_blockhash_fee_calculator(rpc_client)?; + let custodian = custodian.map(|a| a.keypair()).unwrap_or(&config.keypair); + // If new custodian is not explicitly set, default to current custodian + if lockup.custodian == Pubkey::default() { + lockup.custodian = custodian.pubkey(); + } + let ixs = vec![stake_instruction::set_lockup( + stake_account_pubkey, + lockup, + &custodian.pubkey(), + )]; + let (nonce_authority, nonce_authority_pubkey) = nonce_authority + .map(|a| (a.keypair(), a.pubkey())) + .unwrap_or((&config.keypair, config.keypair.pubkey())); + let fee_payer = fee_payer.map(|f| f.keypair()).unwrap_or(&config.keypair); + let mut tx = if let Some(nonce_account) = &nonce_account { + Transaction::new_signed_with_nonce( + ixs, + Some(&fee_payer.pubkey()), + &[fee_payer, nonce_authority, custodian], + nonce_account, + &nonce_authority.pubkey(), + recent_blockhash, + ) + } else { + Transaction::new_signed_with_payer( + ixs, + Some(&fee_payer.pubkey()), + &[fee_payer, custodian], + recent_blockhash, + ) + }; + if let Some(signers) = signers { + replace_signatures(&mut tx, &signers)?; + } + if sign_only { + return_signers(&tx) + } else { + if let Some(nonce_account) = &nonce_account { + let nonce_account = rpc_client.get_account(nonce_account)?; + check_nonce_account(&nonce_account, &nonce_authority_pubkey, &recent_blockhash)?; + } + check_account_for_fee( + rpc_client, + &tx.message.account_keys[0], + &fee_calculator, + &tx.message, + )?; + let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); + log_instruction_custom_error::(result) + } +} + pub fn print_stake_state(stake_lamports: u64, stake_state: &StakeState, use_lamports_unit: bool) { fn show_authorized(authorized: &Authorized) { println!("Authorized Staker: {}", authorized.staker); diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index ece6c5b6a1..d9dc880031 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -860,6 +860,7 @@ fn test_stake_split() { config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); let mut config_offline = CliConfig::default(); + config_offline.json_rpc_url = String::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(); @@ -878,7 +879,6 @@ fn test_stake_split() { 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(); @@ -902,7 +902,6 @@ fn test_stake_split() { 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(); @@ -935,7 +934,7 @@ fn test_stake_split() { sign_only: true, signers: None, blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), - nonce_account: Some(nonce_account_pubkey.into()), + nonce_account: Some(nonce_account_pubkey), nonce_authority: None, split_stake_account: read_keypair_file(&split_keypair_file).unwrap().into(), seed: None, @@ -950,7 +949,7 @@ fn test_stake_split() { sign_only: false, signers: Some(signers), blockhash_query: BlockhashQuery::FeeCalculator(blockhash), - nonce_account: Some(nonce_account_pubkey.into()), + nonce_account: Some(nonce_account_pubkey), nonce_authority: Some(offline_pubkey.into()), split_stake_account: read_keypair_file(&split_keypair_file).unwrap().into(), seed: None, @@ -972,3 +971,225 @@ fn test_stake_split() { server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); } + +#[test] +fn test_stake_set_lockup() { + 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(); + config_offline.json_rpc_url = String::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(); + + 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(); + + let mut lockup = Lockup::default(); + lockup.custodian = config.keypair.pubkey(); + + 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, + lamports: 10 * minimum_stake_balance, + }; + process_command(&config).unwrap(); + check_balance( + 10 * minimum_stake_balance, + &rpc_client, + &stake_account_pubkey, + ); + + // Online set lockup + let mut lockup = Lockup { + unix_timestamp: 1581534570, + epoch: 200, + custodian: Pubkey::default(), + }; + config.command = CliCommand::StakeSetLockup { + stake_account_pubkey, + lockup, + custodian: None, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: None, + fee_payer: None, + }; + process_command(&config).unwrap(); + let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); + let stake_state: StakeState = stake_account.state().unwrap(); + let current_lockup = match stake_state { + StakeState::Initialized(meta) => meta.lockup, + _ => panic!("Unexpected stake state!"), + }; + lockup.custodian = config.keypair.pubkey(); // Default new_custodian is config.keypair + assert_eq!(current_lockup, lockup); + + // Set custodian to another pubkey + let online_custodian = Keypair::new(); + let online_custodian_pubkey = online_custodian.pubkey(); + let (online_custodian_file, mut tmp_file) = make_tmp_file(); + write_keypair(&online_custodian, tmp_file.as_file_mut()).unwrap(); + + let lockup = Lockup { + unix_timestamp: 1581534571, + epoch: 201, + custodian: online_custodian_pubkey, + }; + config.command = CliCommand::StakeSetLockup { + stake_account_pubkey, + lockup, + custodian: None, + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: None, + fee_payer: None, + }; + process_command(&config).unwrap(); + + let mut lockup = Lockup { + unix_timestamp: 1581534572, + epoch: 202, + custodian: Pubkey::default(), + }; + config.command = CliCommand::StakeSetLockup { + stake_account_pubkey, + lockup, + custodian: Some(read_keypair_file(&online_custodian_file).unwrap().into()), + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: None, + fee_payer: None, + }; + process_command(&config).unwrap(); + let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); + let stake_state: StakeState = stake_account.state().unwrap(); + let current_lockup = match stake_state { + StakeState::Initialized(meta) => meta.lockup, + _ => panic!("Unexpected stake state!"), + }; + lockup.custodian = online_custodian_pubkey; // Default new_custodian is designated custodian + assert_eq!(current_lockup, lockup); + + // Set custodian to offline pubkey + let lockup = Lockup { + unix_timestamp: 1581534573, + epoch: 203, + custodian: offline_pubkey, + }; + config.command = CliCommand::StakeSetLockup { + stake_account_pubkey, + lockup, + custodian: Some(online_custodian.into()), + sign_only: false, + signers: None, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: None, + fee_payer: None, + }; + process_command(&config).unwrap(); + + // Create nonce account + let minimum_nonce_balance = rpc_client + .get_minimum_balance_for_rent_exemption(NonceState::size()) + .unwrap(); + 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 set lockup + let lockup = Lockup { + unix_timestamp: 1581534576, + epoch: 222, + custodian: offline_pubkey, + }; + config_offline.command = CliCommand::StakeSetLockup { + stake_account_pubkey, + lockup, + custodian: None, + sign_only: true, + signers: None, + blockhash_query: BlockhashQuery::None(nonce_hash, FeeCalculator::default()), + nonce_account: Some(nonce_account_pubkey), + nonce_authority: None, + fee_payer: None, + }; + let sig_response = process_command(&config_offline).unwrap(); + let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); + config.command = CliCommand::StakeSetLockup { + stake_account_pubkey, + lockup, + custodian: Some(offline_pubkey.into()), + sign_only: false, + signers: Some(signers), + blockhash_query: BlockhashQuery::FeeCalculator(blockhash), + nonce_account: Some(nonce_account_pubkey), + nonce_authority: Some(offline_pubkey.into()), + fee_payer: Some(offline_pubkey.into()), + }; + process_command(&config).unwrap(); + let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); + let stake_state: StakeState = stake_account.state().unwrap(); + let current_lockup = match stake_state { + StakeState::Initialized(meta) => meta.lockup, + _ => panic!("Unexpected stake state!"), + }; + assert_eq!(current_lockup, lockup); + + server.close().unwrap(); + remove_dir_all(ledger_path).unwrap(); +}