From aeb30fa8739b3b2f0e56cc7253c272dd14f2c644 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Thu, 15 Jul 2021 16:45:03 -0600 Subject: [PATCH] Cli: Support checked stake and vote operations (#18449) * Refactor VoteAuthorize to use SignerIndex and support vote-authorize-*-checked * Add checked bool const and use in command parsing * Add create-stake-account-checked handling * Add stake-set-lockup-checked handling * Remove unnecessary mut * Add stake-authorized-checked handling --- cli/src/cli.rs | 61 ++- cli/src/stake.rs | 920 +++++++++++++++++++++++++++++++++++++++++---- cli/src/vote.rs | 188 +++++++-- cli/tests/stake.rs | 322 +++++++++++++++- cli/tests/vote.rs | 37 +- 5 files changed, 1412 insertions(+), 116 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 305b7fc653..c3e30d5172 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -45,11 +45,7 @@ use solana_sdk::{ message::Message, pubkey::Pubkey, signature::{Signature, Signer, SignerError}, - stake::{ - self, - instruction::LockupArgs, - state::{Lockup, StakeAuthorize}, - }, + stake::{self, instruction::LockupArgs, state::Lockup}, system_instruction::{self, SystemError}, system_program, transaction::{Transaction, TransactionError}, @@ -64,6 +60,7 @@ use thiserror::Error; pub const DEFAULT_RPC_TIMEOUT_SECONDS: &str = "30"; pub const DEFAULT_CONFIRM_TX_TIMEOUT_SECONDS: &str = "5"; +const CHECKED: bool = true; #[derive(Debug, PartialEq)] #[allow(clippy::large_enum_variant)] @@ -199,6 +196,7 @@ pub enum CliCommand { seed: Option, staker: Option, withdrawer: Option, + withdrawer_signer: Option, lockup: Lockup, amount: SpendAmount, sign_only: bool, @@ -272,7 +270,7 @@ pub enum CliCommand { }, StakeAuthorize { stake_account_pubkey: Pubkey, - new_authorizations: Vec<(StakeAuthorize, Pubkey, SignerIndex)>, + new_authorizations: Vec, sign_only: bool, dump_transaction_message: bool, blockhash_query: BlockhashQuery, @@ -287,6 +285,7 @@ pub enum CliCommand { stake_account_pubkey: Pubkey, lockup: LockupArgs, custodian: SignerIndex, + new_custodian_signer: Option, sign_only: bool, dump_transaction_message: bool, blockhash_query: BlockhashQuery, @@ -344,6 +343,8 @@ pub enum CliCommand { new_authorized_pubkey: Pubkey, vote_authorize: VoteAuthorize, memo: Option, + authorized: SignerIndex, + new_authorized: Option, }, VoteUpdateValidator { vote_account_pubkey: Pubkey, @@ -720,7 +721,10 @@ pub fn parse_command( } // Stake Commands ("create-stake-account", Some(matches)) => { - parse_create_stake_account(matches, default_signer, wallet_manager) + parse_create_stake_account(matches, default_signer, wallet_manager, !CHECKED) + } + ("create-stake-account-checked", Some(matches)) => { + parse_create_stake_account(matches, default_signer, wallet_manager, CHECKED) } ("delegate-stake", Some(matches)) => { parse_stake_delegate_stake(matches, default_signer, wallet_manager) @@ -738,10 +742,16 @@ pub fn parse_command( parse_merge_stake(matches, default_signer, wallet_manager) } ("stake-authorize", Some(matches)) => { - parse_stake_authorize(matches, default_signer, wallet_manager) + parse_stake_authorize(matches, default_signer, wallet_manager, !CHECKED) + } + ("stake-authorize-checked", Some(matches)) => { + parse_stake_authorize(matches, default_signer, wallet_manager, CHECKED) } ("stake-set-lockup", Some(matches)) => { - parse_stake_set_lockup(matches, default_signer, wallet_manager) + parse_stake_set_lockup(matches, default_signer, wallet_manager, !CHECKED) + } + ("stake-set-lockup-checked", Some(matches)) => { + parse_stake_set_lockup(matches, default_signer, wallet_manager, CHECKED) } ("stake-account", Some(matches)) => parse_show_stake_account(matches, wallet_manager), ("stake-history", Some(matches)) => parse_show_stake_history(matches), @@ -768,12 +778,28 @@ pub fn parse_command( default_signer, wallet_manager, VoteAuthorize::Voter, + !CHECKED, ), ("vote-authorize-withdrawer", Some(matches)) => parse_vote_authorize( matches, default_signer, wallet_manager, VoteAuthorize::Withdrawer, + !CHECKED, + ), + ("vote-authorize-voter-checked", Some(matches)) => parse_vote_authorize( + matches, + default_signer, + wallet_manager, + VoteAuthorize::Voter, + CHECKED, + ), + ("vote-authorize-withdrawer-checked", Some(matches)) => parse_vote_authorize( + matches, + default_signer, + wallet_manager, + VoteAuthorize::Withdrawer, + CHECKED, ), ("vote-account", Some(matches)) => parse_vote_get_account_command(matches, wallet_manager), ("withdraw-from-vote-account", Some(matches)) => { @@ -1534,6 +1560,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { seed, staker, withdrawer, + withdrawer_signer, lockup, amount, sign_only, @@ -1551,6 +1578,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { seed, staker, withdrawer, + *withdrawer_signer, lockup, *amount, *sign_only, @@ -1712,8 +1740,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { ), CliCommand::StakeSetLockup { stake_account_pubkey, - mut lockup, + lockup, custodian, + new_custodian_signer, sign_only, dump_transaction_message, blockhash_query, @@ -1725,7 +1754,8 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { &rpc_client, config, stake_account_pubkey, - &mut lockup, + lockup, + *new_custodian_signer, *custodian, *sign_only, *dump_transaction_message, @@ -1839,12 +1869,16 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { new_authorized_pubkey, vote_authorize, memo, + authorized, + new_authorized, } => process_vote_authorize( &rpc_client, config, vote_account_pubkey, new_authorized_pubkey, *vote_authorize, + *authorized, + *new_authorized, memo.as_ref(), ), CliCommand::VoteUpdateValidator { @@ -2648,6 +2682,8 @@ mod tests { new_authorized_pubkey, vote_authorize: VoteAuthorize::Voter, memo: None, + authorized: 0, + new_authorized: None, }; let result = process_command(&config); assert!(result.is_ok()); @@ -2671,6 +2707,7 @@ mod tests { seed: None, staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup { epoch: 0, unix_timestamp: 0, @@ -2843,6 +2880,8 @@ mod tests { new_authorized_pubkey: bob_pubkey, vote_authorize: VoteAuthorize::Voter, memo: None, + authorized: 0, + new_authorized: None, }; assert!(process_command(&config).is_err()); diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 2e5855f2aa..e28597e876 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -96,6 +96,20 @@ fn custodian_arg<'a, 'b>() -> Arg<'a, 'b> { .help(CUSTODIAN_ARG.help) } +pub(crate) struct StakeAuthorization { + authorization_type: StakeAuthorize, + new_authority_pubkey: Pubkey, + authority_pubkey: Option, +} + +#[derive(Debug, PartialEq)] +pub struct StakeAuthorizationIndexed { + pub authorization_type: StakeAuthorize, + pub new_authority_pubkey: Pubkey, + pub authority: SignerIndex, + pub new_authority_signer: Option, +} + pub trait StakeSubCommands { fn stake_subcommands(self) -> Self; } @@ -181,6 +195,64 @@ impl StakeSubCommands for App<'_, '_> { .arg(fee_payer_arg()) .arg(memo_arg()) ) + .subcommand( + SubCommand::with_name("create-stake-account-checked") + .about("Create a stake account, checking the withdraw authority as a signer") + .arg( + Arg::with_name("stake_account") + .index(1) + .value_name("STAKE_ACCOUNT_KEYPAIR") + .takes_value(true) + .required(true) + .validator(is_valid_signer) + .help("Stake account to create (or base of derived address if --seed is used)") + ) + .arg( + Arg::with_name("amount") + .index(2) + .value_name("AMOUNT") + .takes_value(true) + .validator(is_amount_or_all) + .required(true) + .help("The amount to send to the stake account, in SOL; accepts keyword ALL") + ) + .arg( + Arg::with_name("seed") + .long("seed") + .value_name("STRING") + .takes_value(true) + .help("Seed for address generation; if specified, the resulting account \ + will be at a derived address of the STAKE_ACCOUNT_KEYPAIR pubkey") + ) + .arg( + Arg::with_name(STAKE_AUTHORITY_ARG.name) + .long(STAKE_AUTHORITY_ARG.long) + .value_name("PUBKEY") + .takes_value(true) + .validator(is_valid_pubkey) + .help(STAKE_AUTHORITY_ARG.help) + ) + .arg( + Arg::with_name(WITHDRAW_AUTHORITY_ARG.name) + .long(WITHDRAW_AUTHORITY_ARG.long) + .value_name("KEYPAIR") + .takes_value(true) + .validator(is_valid_signer) + .help(WITHDRAW_AUTHORITY_ARG.help) + ) + .arg( + Arg::with_name("from") + .long("from") + .takes_value(true) + .value_name("KEYPAIR") + .validator(is_valid_signer) + .help("Source account of funds [default: cli config keypair]"), + ) + .offline_args() + .nonce_args(false) + .arg(fee_payer_arg()) + .arg(memo_arg()) + ) .subcommand( SubCommand::with_name("delegate-stake") .about("Delegate stake to a vote account") @@ -249,6 +321,46 @@ impl StakeSubCommands for App<'_, '_> { ) .arg(memo_arg()) ) + .subcommand( + SubCommand::with_name("stake-authorize-checked") + .about("Authorize a new signing keypair for the given stake account, checking the authority as a signer") + .arg( + pubkey!(Arg::with_name("stake_account_pubkey") + .required(true) + .index(1) + .value_name("STAKE_ACCOUNT_ADDRESS"), + "Stake account in which to set a new authority. ") + ) + .arg( + Arg::with_name("new_stake_authority") + .long("new-stake-authority") + .value_name("KEYPAIR") + .takes_value(true) + .validator(is_valid_signer) + .help("New authorized staker") + ) + .arg( + Arg::with_name("new_withdraw_authority") + .long("new-withdraw-authority") + .value_name("KEYPAIR") + .takes_value(true) + .validator(is_valid_signer) + .help("New authorized withdrawer") + ) + .arg(stake_authority_arg()) + .arg(withdraw_authority_arg()) + .offline_args() + .nonce_args(false) + .arg(fee_payer_arg()) + .arg(custodian_arg()) + .arg( + Arg::with_name("no_wait") + .long("no-wait") + .takes_value(false) + .help("Return signature immediately after submitting the transaction, instead of waiting for confirmations"), + ) + .arg(memo_arg()) + ) .subcommand( SubCommand::with_name("deactivate-stake") .about("Deactivate the delegated stake from the stake account") @@ -428,6 +540,56 @@ impl StakeSubCommands for App<'_, '_> { .arg(fee_payer_arg()) .arg(memo_arg()) ) + .subcommand( + SubCommand::with_name("stake-set-lockup-checked") + .about("Set Lockup for the stake account, checking the new authority as a signer") + .arg( + pubkey!(Arg::with_name("stake_account_pubkey") + .index(1) + .value_name("STAKE_ACCOUNT_ADDRESS") + .required(true), + "Stake account for which to set lockup parameters. ") + ) + .arg( + Arg::with_name("lockup_epoch") + .long("lockup-epoch") + .value_name("NUMBER") + .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 DATETIME") + .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") + .takes_value(true) + .validator(is_valid_signer) + .help("Keypair of a new lockup custodian") + ) + .group(ArgGroup::with_name("lockup_details") + .args(&["lockup_epoch", "lockup_date", "new_custodian"]) + .multiple(true) + .required(true)) + .arg( + Arg::with_name("custodian") + .long("custodian") + .takes_value(true) + .value_name("KEYPAIR") + .validator(is_valid_signer) + .help("Keypair of the existing custodian [default: cli config pubkey]") + ) + .offline_args() + .nonce_args(false) + .arg(fee_payer_arg()) + .arg(memo_arg()) + ) .subcommand( SubCommand::with_name("stake-account") .about("Show the contents of a stake account") @@ -493,13 +655,23 @@ pub fn parse_create_stake_account( matches: &ArgMatches<'_>, default_signer: &DefaultSigner, wallet_manager: &mut Option>, + checked: bool, ) -> Result { let seed = matches.value_of("seed").map(|s| s.to_string()); 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 custodian = pubkey_of_signer(matches, "custodian", wallet_manager)?.unwrap_or_default(); let staker = pubkey_of_signer(matches, STAKE_AUTHORITY_ARG.name, wallet_manager)?; - let withdrawer = pubkey_of_signer(matches, WITHDRAW_AUTHORITY_ARG.name, wallet_manager)?; + + let (withdrawer_signer, withdrawer) = if checked { + signer_of(matches, WITHDRAW_AUTHORITY_ARG.name, wallet_manager)? + } else { + ( + None, + pubkey_of_signer(matches, WITHDRAW_AUTHORITY_ARG.name, wallet_manager)?, + ) + }; + let amount = SpendAmount::new_from_matches(matches, "amount"); let sign_only = matches.is_present(SIGN_ONLY_ARG.name); let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name); @@ -517,6 +689,9 @@ pub fn parse_create_stake_account( if nonce_account.is_some() { bulk_signers.push(nonce_authority); } + if withdrawer_signer.is_some() { + bulk_signers.push(withdrawer_signer); + } let signer_info = default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; @@ -526,6 +701,11 @@ pub fn parse_create_stake_account( seed, staker, withdrawer, + withdrawer_signer: if checked { + signer_info.index_of(withdrawer) + } else { + None + }, lockup: Lockup { unix_timestamp, epoch, @@ -595,15 +775,24 @@ pub fn parse_stake_authorize( matches: &ArgMatches<'_>, default_signer: &DefaultSigner, wallet_manager: &mut Option>, + checked: bool, ) -> Result { let stake_account_pubkey = pubkey_of_signer(matches, "stake_account_pubkey", wallet_manager)?.unwrap(); let mut new_authorizations = Vec::new(); let mut bulk_signers = Vec::new(); - if let Some(new_authority_pubkey) = - pubkey_of_signer(matches, "new_stake_authority", wallet_manager)? - { + + let (new_staker_signer, new_staker) = if checked { + signer_of(matches, "new_stake_authority", wallet_manager)? + } else { + ( + None, + pubkey_of_signer(matches, "new_stake_authority", wallet_manager)?, + ) + }; + + if let Some(new_authority_pubkey) = new_staker { let (authority, authority_pubkey) = { let (authority, authority_pubkey) = signer_of(matches, STAKE_AUTHORITY_ARG.name, wallet_manager)?; @@ -614,24 +803,38 @@ pub fn parse_stake_authorize( (authority, authority_pubkey) } }; - new_authorizations.push(( - StakeAuthorize::Staker, + new_authorizations.push(StakeAuthorization { + authorization_type: StakeAuthorize::Staker, new_authority_pubkey, authority_pubkey, - )); + }); bulk_signers.push(authority); + if new_staker.is_some() { + bulk_signers.push(new_staker_signer); + } }; - if let Some(new_authority_pubkey) = - pubkey_of_signer(matches, "new_withdraw_authority", wallet_manager)? - { + + let (new_withdrawer_signer, new_withdrawer) = if checked { + signer_of(matches, "new_withdraw_authority", wallet_manager)? + } else { + ( + None, + pubkey_of_signer(matches, "new_withdraw_authority", wallet_manager)?, + ) + }; + + if let Some(new_authority_pubkey) = new_withdrawer { let (authority, authority_pubkey) = signer_of(matches, WITHDRAW_AUTHORITY_ARG.name, wallet_manager)?; - new_authorizations.push(( - StakeAuthorize::Withdrawer, + new_authorizations.push(StakeAuthorization { + authorization_type: StakeAuthorize::Withdrawer, new_authority_pubkey, authority_pubkey, - )); + }); bulk_signers.push(authority); + if new_withdrawer_signer.is_some() { + bulk_signers.push(new_withdrawer_signer); + } }; let sign_only = matches.is_present(SIGN_ONLY_ARG.name); let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name); @@ -657,12 +860,17 @@ pub fn parse_stake_authorize( let new_authorizations = new_authorizations .into_iter() .map( - |(stake_authorize, new_authority_pubkey, authority_pubkey)| { - ( - stake_authorize, + |StakeAuthorization { + authorization_type, + new_authority_pubkey, + authority_pubkey, + }| { + StakeAuthorizationIndexed { + authorization_type, new_authority_pubkey, - signer_info.index_of(authority_pubkey).unwrap(), - ) + authority: signer_info.index_of(authority_pubkey).unwrap(), + new_authority_signer: signer_info.index_of(Some(new_authority_pubkey)), + } }, ) .collect(); @@ -879,12 +1087,21 @@ pub fn parse_stake_set_lockup( matches: &ArgMatches<'_>, default_signer: &DefaultSigner, wallet_manager: &mut Option>, + checked: bool, ) -> Result { let stake_account_pubkey = pubkey_of_signer(matches, "stake_account_pubkey", wallet_manager)?.unwrap(); let epoch = value_of(matches, "lockup_epoch"); let unix_timestamp = unix_timestamp_from_rfc3339_datetime(matches, "lockup_date"); - let new_custodian = pubkey_of_signer(matches, "new_custodian", wallet_manager)?; + + let (new_custodian_signer, new_custodian) = if checked { + signer_of(matches, "new_custodian", wallet_manager)? + } else { + ( + None, + pubkey_of_signer(matches, "new_custodian", wallet_manager)?, + ) + }; let sign_only = matches.is_present(SIGN_ONLY_ARG.name); let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name); @@ -901,6 +1118,9 @@ pub fn parse_stake_set_lockup( if nonce_account.is_some() { bulk_signers.push(nonce_authority); } + if new_custodian_signer.is_some() { + bulk_signers.push(new_custodian_signer); + } let signer_info = default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; @@ -912,6 +1132,11 @@ pub fn parse_stake_set_lockup( epoch, unix_timestamp, }, + new_custodian_signer: if checked { + signer_info.index_of(new_custodian) + } else { + None + }, custodian: signer_info.index_of(custodian_pubkey).unwrap(), sign_only, dump_transaction_message, @@ -967,6 +1192,7 @@ pub fn process_create_stake_account( seed: &Option, staker: &Option, withdrawer: &Option, + withdrawer_signer: Option, lockup: &Lockup, amount: SpendAmount, sign_only: bool, @@ -999,8 +1225,18 @@ pub fn process_create_stake_account( withdrawer: withdrawer.unwrap_or(from.pubkey()), }; - let ixs = if let Some(seed) = seed { - stake_instruction::create_account_with_seed( + let ixs = match (seed, withdrawer_signer) { + (Some(seed), Some(_withdrawer_signer)) => { + stake_instruction::create_account_with_seed_checked( + &from.pubkey(), // from + &stake_account_address, // to + &stake_account.pubkey(), // base + seed, // seed + &authorized, + lamports, + ) + } + (Some(seed), None) => stake_instruction::create_account_with_seed( &from.pubkey(), // from &stake_account_address, // to &stake_account.pubkey(), // base @@ -1008,18 +1244,22 @@ pub fn process_create_stake_account( &authorized, lockup, lamports, - ) - .with_memo(memo) - } else { - stake_instruction::create_account( + ), + (None, Some(_withdrawer_signer)) => stake_instruction::create_account_checked( + &from.pubkey(), + &stake_account.pubkey(), + &authorized, + lamports, + ), + (None, None) => stake_instruction::create_account( &from.pubkey(), &stake_account.pubkey(), &authorized, lockup, lamports, - ) - .with_memo(memo) - }; + ), + } + .with_memo(memo); if let Some(nonce_account) = &nonce_account { Message::new_with_nonce( ixs, @@ -1102,7 +1342,7 @@ pub fn process_stake_authorize( rpc_client: &RpcClient, config: &CliConfig, stake_account_pubkey: &Pubkey, - new_authorizations: &[(StakeAuthorize, Pubkey, SignerIndex)], + new_authorizations: &[StakeAuthorizationIndexed], custodian: Option, sign_only: bool, dump_transaction_message: bool, @@ -1115,19 +1355,35 @@ pub fn process_stake_authorize( ) -> ProcessResult { let mut ixs = Vec::new(); let custodian = custodian.map(|index| config.signers[index]); - for (stake_authorize, authorized_pubkey, authority) in new_authorizations.iter() { + for StakeAuthorizationIndexed { + authorization_type, + new_authority_pubkey, + authority, + new_authority_signer, + } in new_authorizations.iter() + { check_unique_pubkeys( (stake_account_pubkey, "stake_account_pubkey".to_string()), - (authorized_pubkey, "new_authorized_pubkey".to_string()), + (new_authority_pubkey, "new_authorized_pubkey".to_string()), )?; let authority = config.signers[*authority]; - ixs.push(stake_instruction::authorize( - stake_account_pubkey, // stake account to update - &authority.pubkey(), // currently authorized - authorized_pubkey, // new stake signer - *stake_authorize, // stake or withdraw - custodian.map(|signer| signer.pubkey()).as_ref(), - )); + if new_authority_signer.is_some() { + ixs.push(stake_instruction::authorize_checked( + stake_account_pubkey, // stake account to update + &authority.pubkey(), // currently authorized + new_authority_pubkey, // new stake signer + *authorization_type, // stake or withdraw + custodian.map(|signer| signer.pubkey()).as_ref(), + )); + } else { + ixs.push(stake_instruction::authorize( + stake_account_pubkey, // stake account to update + &authority.pubkey(), // currently authorized + new_authority_pubkey, // new stake signer + *authorization_type, // stake or withdraw + custodian.map(|signer| signer.pubkey()).as_ref(), + )); + } } ixs = ixs.with_memo(memo); @@ -1620,7 +1876,8 @@ pub fn process_stake_set_lockup( rpc_client: &RpcClient, config: &CliConfig, stake_account_pubkey: &Pubkey, - lockup: &mut LockupArgs, + lockup: &LockupArgs, + new_custodian_signer: Option, custodian: SignerIndex, sign_only: bool, dump_transaction_message: bool, @@ -1634,11 +1891,11 @@ pub fn process_stake_set_lockup( blockhash_query.get_blockhash_and_fee_calculator(rpc_client, config.commitment)?; let custodian = config.signers[custodian]; - let ixs = vec![stake_instruction::set_lockup( - stake_account_pubkey, - lockup, - &custodian.pubkey(), - )] + let ixs = vec![if new_custodian_signer.is_some() { + stake_instruction::set_lockup_checked(stake_account_pubkey, lockup, &custodian.pubkey()) + } else { + stake_instruction::set_lockup(stake_account_pubkey, lockup, &custodian.pubkey()) + }] .with_memo(memo); let nonce_authority = config.signers[nonce_authority]; let fee_payer = config.signers[fee_payer]; @@ -2156,8 +2413,18 @@ mod tests { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorizations: vec![ - (StakeAuthorize::Staker, new_stake_authority, 0,), - (StakeAuthorize::Withdrawer, new_withdraw_authority, 0,), + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: new_stake_authority, + authority: 0, + new_authority_signer: None, + }, + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: new_withdraw_authority, + authority: 0, + new_authority_signer: None, + }, ], sign_only: false, dump_transaction_message: false, @@ -2194,8 +2461,18 @@ mod tests { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorizations: vec![ - (StakeAuthorize::Staker, new_stake_authority, 1,), - (StakeAuthorize::Withdrawer, new_withdraw_authority, 2,), + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: new_stake_authority, + authority: 1, + new_authority_signer: None, + }, + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: new_withdraw_authority, + authority: 2, + new_authority_signer: None, + }, ], sign_only: false, dump_transaction_message: false, @@ -2236,8 +2513,18 @@ mod tests { command: CliCommand::StakeAuthorize { stake_account_pubkey, new_authorizations: vec![ - (StakeAuthorize::Staker, new_stake_authority, 1,), - (StakeAuthorize::Withdrawer, new_withdraw_authority, 1,), + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: new_stake_authority, + authority: 1, + new_authority_signer: None, + }, + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: new_withdraw_authority, + authority: 1, + new_authority_signer: None, + }, ], sign_only: false, dump_transaction_message: false, @@ -2269,7 +2556,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, new_stake_authority, 0,),], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: new_stake_authority, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), @@ -2297,7 +2589,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, new_stake_authority, 1,),], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: new_stake_authority, + authority: 1, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), @@ -2331,7 +2628,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, new_stake_authority, 1,),], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: new_stake_authority, + authority: 1, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), @@ -2362,11 +2664,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![( - StakeAuthorize::Withdrawer, - new_withdraw_authority, - 0, - ),], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: new_withdraw_authority, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), @@ -2394,11 +2697,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![( - StakeAuthorize::Withdrawer, - new_withdraw_authority, - 1, - ),], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: new_withdraw_authority, + authority: 1, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), @@ -2432,7 +2736,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: stake_account_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), @@ -2447,6 +2756,385 @@ mod tests { } ); + // stake-authorize-checked subcommand + let (authority_keypair_file, mut tmp_file) = make_tmp_file(); + let authority_keypair = Keypair::new(); + write_keypair(&authority_keypair, tmp_file.as_file_mut()).unwrap(); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize-checked", + &stake_account_string, + "--new-stake-authority", + &authority_keypair_file, + "--new-withdraw-authority", + &authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![ + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 0, + new_authority_signer: Some(1), + }, + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 0, + new_authority_signer: Some(1), + }, + ], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into(), + ], + }, + ); + let (withdraw_authority_keypair_file, mut tmp_file) = make_tmp_file(); + let withdraw_authority_keypair = Keypair::new(); + write_keypair(&withdraw_authority_keypair, tmp_file.as_file_mut()).unwrap(); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize-checked", + &stake_account_string, + "--new-stake-authority", + &authority_keypair_file, + "--new-withdraw-authority", + &authority_keypair_file, + "--stake-authority", + &stake_authority_keypair_file, + "--withdraw-authority", + &withdraw_authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![ + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 1, + new_authority_signer: Some(2), + }, + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 3, + new_authority_signer: Some(2), + }, + ], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&stake_authority_keypair_file) + .unwrap() + .into(), + read_keypair_file(&authority_keypair_file).unwrap().into(), + read_keypair_file(&withdraw_authority_keypair_file) + .unwrap() + .into(), + ], + }, + ); + // Withdraw authority may set both new authorities + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize-checked", + &stake_account_string, + "--new-stake-authority", + &authority_keypair_file, + "--new-withdraw-authority", + &authority_keypair_file, + "--withdraw-authority", + &withdraw_authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![ + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 1, + new_authority_signer: Some(2), + }, + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 1, + new_authority_signer: Some(2), + }, + ], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&withdraw_authority_keypair_file) + .unwrap() + .into(), + read_keypair_file(&authority_keypair_file).unwrap().into(), + ], + }, + ); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize-checked", + &stake_account_string, + "--new-stake-authority", + &authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 0, + new_authority_signer: Some(1), + }], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into(), + ], + }, + ); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize-checked", + &stake_account_string, + "--new-stake-authority", + &authority_keypair_file, + "--stake-authority", + &stake_authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 1, + new_authority_signer: Some(2), + }], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&stake_authority_keypair_file) + .unwrap() + .into(), + read_keypair_file(&authority_keypair_file).unwrap().into(), + ], + }, + ); + // Withdraw authority may set new stake authority + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize-checked", + &stake_account_string, + "--new-stake-authority", + &authority_keypair_file, + "--withdraw-authority", + &withdraw_authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 1, + new_authority_signer: Some(2), + }], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&withdraw_authority_keypair_file) + .unwrap() + .into(), + read_keypair_file(&authority_keypair_file).unwrap().into(), + ], + }, + ); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize-checked", + &stake_account_string, + "--new-withdraw-authority", + &authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 0, + new_authority_signer: Some(1), + }], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into(), + ], + }, + ); + let test_stake_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize-checked", + &stake_account_string, + "--new-withdraw-authority", + &authority_keypair_file, + "--withdraw-authority", + &withdraw_authority_keypair_file, + ]); + assert_eq!( + parse_command(&test_stake_authorize, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 1, + new_authority_signer: Some(2), + }], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&withdraw_authority_keypair_file) + .unwrap() + .into(), + read_keypair_file(&authority_keypair_file).unwrap().into(), + ], + }, + ); + + // Test Authorize Subcommand w/ no-wait + let test_authorize = test_commands.clone().get_matches_from(vec![ + "test", + "stake-authorize-checked", + &stake_account_string, + "--new-stake-authority", + &authority_keypair_file, + "--no-wait", + ]); + assert_eq!( + parse_command(&test_authorize, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: authority_keypair.pubkey(), + authority: 0, + new_authority_signer: Some(1), + }], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: true, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&authority_keypair_file).unwrap().into(), + ], + } + ); + // Test Authorize Subcommand w/ sign-only let blockhash = Hash::default(); let blockhash_string = format!("{}", blockhash); @@ -2465,7 +3153,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: stake_account_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: true, dump_transaction_message: false, blockhash_query: BlockhashQuery::None(blockhash), @@ -2502,7 +3195,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: stake_account_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::FeeCalculator( @@ -2552,7 +3250,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: stake_account_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::FeeCalculator( @@ -2588,7 +3291,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: stake_account_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::FeeCalculator( @@ -2629,7 +3337,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: stake_account_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::FeeCalculator( @@ -2669,7 +3382,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: stake_account_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), @@ -2707,7 +3425,12 @@ mod tests { CliCommandInfo { command: CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, stake_account_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: stake_account_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::FeeCalculator( @@ -2755,6 +3478,7 @@ mod tests { seed: None, staker: Some(authorized), withdrawer: Some(authorized), + withdrawer_signer: None, lockup: Lockup { epoch: 43, unix_timestamp: 0, @@ -2798,6 +3522,7 @@ mod tests { seed: None, staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000_000_000), sign_only: false, @@ -2815,6 +3540,58 @@ mod tests { ], } ); + let (withdrawer_keypair_file, mut tmp_file) = make_tmp_file(); + let withdrawer_keypair = Keypair::new(); + write_keypair(&withdrawer_keypair, tmp_file.as_file_mut()).unwrap(); + let test_create_stake_account = test_commands.clone().get_matches_from(vec![ + "test", + "create-stake-account-checked", + &keypair_file, + "50", + "--stake-authority", + &authorized_string, + "--withdraw-authority", + &withdrawer_keypair_file, + ]); + assert_eq!( + parse_command(&test_create_stake_account, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::CreateStakeAccount { + stake_account: 1, + seed: None, + staker: Some(authorized), + withdrawer: Some(withdrawer_keypair.pubkey()), + withdrawer_signer: Some(2), + lockup: Lockup::default(), + amount: SpendAmount::Some(50_000_000_000), + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + from: 0, + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + stake_account_keypair.into(), + withdrawer_keypair.into(), + ], + } + ); + + let test_create_stake_account = test_commands.clone().get_matches_from(vec![ + "test", + "create-stake-account-checked", + &keypair_file, + "50", + "--stake-authority", + &authorized_string, + "--withdraw-authority", + &authorized_string, + ]); + assert!(parse_command(&test_create_stake_account, &default_signer, &mut None).is_err()); // CreateStakeAccount offline and nonce let nonce_account = Pubkey::new(&[1u8; 32]); @@ -2853,6 +3630,7 @@ mod tests { seed: None, staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000_000_000), sign_only: false, diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 079386ad5b..ce27c55cf1 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -138,6 +138,64 @@ impl VoteSubCommands for App<'_, '_> { ) .arg(memo_arg()) ) + .subcommand( + SubCommand::with_name("vote-authorize-voter-checked") + .about("Authorize a new vote signing keypair for the given vote account, \ + checking the new authority as a signer") + .arg( + pubkey!(Arg::with_name("vote_account_pubkey") + .index(1) + .value_name("VOTE_ACCOUNT_ADDRESS") + .required(true), + "Vote account in which to set the authorized voter. "), + ) + .arg( + Arg::with_name("authorized") + .index(2) + .value_name("AUTHORIZED_KEYPAIR") + .required(true) + .validator(is_valid_signer) + .help("Current authorized vote signer."), + ) + .arg( + Arg::with_name("new_authorized") + .index(3) + .value_name("NEW_AUTHORIZED_KEYPAIR") + .required(true) + .validator(is_valid_signer) + .help("New authorized vote signer."), + ) + .arg(memo_arg()) + ) + .subcommand( + SubCommand::with_name("vote-authorize-withdrawer-checked") + .about("Authorize a new withdraw signing keypair for the given vote account, \ + checking the new authority as a signer") + .arg( + pubkey!(Arg::with_name("vote_account_pubkey") + .index(1) + .value_name("VOTE_ACCOUNT_ADDRESS") + .required(true), + "Vote account in which to set the authorized withdrawer. "), + ) + .arg( + Arg::with_name("authorized") + .index(2) + .value_name("AUTHORIZED_KEYPAIR") + .required(true) + .validator(is_valid_signer) + .help("Current authorized withdrawer."), + ) + .arg( + Arg::with_name("new_authorized") + .index(3) + .value_name("NEW_AUTHORIZED_KEYPAIR") + .required(true) + .validator(is_valid_signer) + .help("New authorized withdrawer."), + ) + .arg(memo_arg()) + ) .subcommand( SubCommand::with_name("vote-update-validator") .about("Update the vote account's validator identity") @@ -311,19 +369,25 @@ pub fn parse_vote_authorize( default_signer: &DefaultSigner, wallet_manager: &mut Option>, vote_authorize: VoteAuthorize, + checked: bool, ) -> Result { let vote_account_pubkey = pubkey_of_signer(matches, "vote_account_pubkey", wallet_manager)?.unwrap(); - let new_authorized_pubkey = - pubkey_of_signer(matches, "new_authorized_pubkey", wallet_manager)?.unwrap(); - let (authorized, _) = signer_of(matches, "authorized", wallet_manager)?; + let (authorized, authorized_pubkey) = signer_of(matches, "authorized", wallet_manager)?; let payer_provided = None; - let signer_info = default_signer.generate_unique_signers( - vec![payer_provided, authorized], - matches, - wallet_manager, - )?; + let mut signers = vec![payer_provided, authorized]; + + let new_authorized_pubkey = if checked { + let (new_authorized_signer, new_authorized_pubkey) = + signer_of(matches, "new_authorized", wallet_manager)?; + signers.push(new_authorized_signer); + new_authorized_pubkey.unwrap() + } else { + pubkey_of_signer(matches, "new_authorized_pubkey", wallet_manager)?.unwrap() + }; + + let signer_info = default_signer.generate_unique_signers(signers, matches, wallet_manager)?; let memo = matches.value_of(MEMO_ARG.name).map(String::from); Ok(CliCommandInfo { @@ -332,6 +396,12 @@ pub fn parse_vote_authorize( new_authorized_pubkey, vote_authorize, memo, + authorized: signer_info.index_of(authorized_pubkey).unwrap(), + new_authorized: if checked { + signer_info.index_of(Some(new_authorized_pubkey)) + } else { + None + }, }, signers: signer_info.signers, }) @@ -558,28 +628,34 @@ pub fn process_vote_authorize( vote_account_pubkey: &Pubkey, new_authorized_pubkey: &Pubkey, vote_authorize: VoteAuthorize, + authorized: SignerIndex, + new_authorized: Option, memo: Option<&String>, ) -> ProcessResult { - // If the `authorized_account` is also the fee payer, `config.signers` will only have one - // keypair in it - let authorized = if config.signers.len() == 2 { - config.signers[1] - } else { - config.signers[0] - }; + let authorized = config.signers[authorized]; + let new_authorized_signer = new_authorized.map(|index| config.signers[index]); check_unique_pubkeys( (&authorized.pubkey(), "authorized_account".to_string()), (new_authorized_pubkey, "new_authorized_pubkey".to_string()), )?; let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let ixs = vec![vote_instruction::authorize( - vote_account_pubkey, // vote account to update - &authorized.pubkey(), // current authorized - new_authorized_pubkey, // new vote signer/withdrawer - vote_authorize, // vote or withdraw - )] - .with_memo(memo); + let vote_ix = if new_authorized_signer.is_some() { + vote_instruction::authorize_checked( + vote_account_pubkey, // vote account to update + &authorized.pubkey(), // current authorized + new_authorized_pubkey, // new vote signer/withdrawer + vote_authorize, // vote or withdraw + ) + } else { + vote_instruction::authorize( + vote_account_pubkey, // vote account to update + &authorized.pubkey(), // current authorized + new_authorized_pubkey, // new vote signer/withdrawer + vote_authorize, // vote or withdraw + ) + }; + let ixs = vec![vote_ix].with_memo(memo); let message = Message::new(&ixs, Some(&config.signers[0].pubkey())); let mut tx = Transaction::new_unsigned(message); @@ -843,6 +919,8 @@ mod tests { new_authorized_pubkey: pubkey2, vote_authorize: VoteAuthorize::Voter, memo: None, + authorized: 0, + new_authorized: None, }, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } @@ -867,6 +945,8 @@ mod tests { new_authorized_pubkey: pubkey2, vote_authorize: VoteAuthorize::Voter, memo: None, + authorized: 1, + new_authorized: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -875,6 +955,70 @@ mod tests { } ); + let (voter_keypair_file, mut tmp_file) = make_tmp_file(); + let voter_keypair = Keypair::new(); + write_keypair(&voter_keypair, tmp_file.as_file_mut()).unwrap(); + + let test_authorize_voter = test_commands.clone().get_matches_from(vec![ + "test", + "vote-authorize-voter-checked", + &pubkey_string, + &default_keypair_file, + &voter_keypair_file, + ]); + assert_eq!( + parse_command(&test_authorize_voter, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::VoteAuthorize { + vote_account_pubkey: pubkey, + new_authorized_pubkey: voter_keypair.pubkey(), + vote_authorize: VoteAuthorize::Voter, + memo: None, + authorized: 0, + new_authorized: Some(1), + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&voter_keypair_file).unwrap().into() + ], + } + ); + + let test_authorize_voter = test_commands.clone().get_matches_from(vec![ + "test", + "vote-authorize-voter-checked", + &pubkey_string, + &authorized_keypair_file, + &voter_keypair_file, + ]); + assert_eq!( + parse_command(&test_authorize_voter, &default_signer, &mut None).unwrap(), + CliCommandInfo { + command: CliCommand::VoteAuthorize { + vote_account_pubkey: pubkey, + new_authorized_pubkey: voter_keypair.pubkey(), + vote_authorize: VoteAuthorize::Voter, + memo: None, + authorized: 1, + new_authorized: Some(2), + }, + signers: vec![ + read_keypair_file(&default_keypair_file).unwrap().into(), + read_keypair_file(&authorized_keypair_file).unwrap().into(), + read_keypair_file(&voter_keypair_file).unwrap().into(), + ], + } + ); + + let test_authorize_voter = test_commands.clone().get_matches_from(vec![ + "test", + "vote-authorize-voter-checked", + &pubkey_string, + &authorized_keypair_file, + &pubkey2_string, + ]); + assert!(parse_command(&test_authorize_voter, &default_signer, &mut None).is_err()); + let (keypair_file, mut tmp_file) = make_tmp_file(); let keypair = Keypair::new(); write_keypair(&keypair, tmp_file.as_file_mut()).unwrap(); diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 162aa45134..f66b4723cb 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -1,6 +1,7 @@ use solana_cli::{ cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, spend_utils::SpendAmount, + stake::StakeAuthorizationIndexed, test_utils::{check_ready, check_recent_balance}, }; use solana_cli_output::{parse_sign_only_reply_string, OutputFormat}; @@ -64,6 +65,7 @@ fn test_stake_delegation_force() { seed: None, staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: false, @@ -151,6 +153,7 @@ fn test_seed_stake_delegation_and_deactivation() { seed: Some("hi there".to_string()), staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: false, @@ -231,6 +234,7 @@ fn test_stake_delegation_and_deactivation() { seed: None, staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: false, @@ -332,6 +336,7 @@ fn test_offline_stake_delegation_and_deactivation() { seed: None, staker: Some(config_offline.signers[0].pubkey()), withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: false, @@ -451,6 +456,7 @@ fn test_nonced_stake_delegation_and_deactivation() { seed: None, staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: false, @@ -581,6 +587,7 @@ fn test_stake_authorize() { seed: None, staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: false, @@ -600,7 +607,12 @@ fn test_stake_authorize() { config.signers.pop(); config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, online_authority_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: online_authority_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::default(), @@ -629,8 +641,18 @@ fn test_stake_authorize() { config.command = CliCommand::StakeAuthorize { stake_account_pubkey, new_authorizations: vec![ - (StakeAuthorize::Staker, online_authority2_pubkey, 1), - (StakeAuthorize::Withdrawer, withdraw_authority_pubkey, 0), + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: online_authority2_pubkey, + authority: 1, + new_authority_signer: None, + }, + StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: withdraw_authority_pubkey, + authority: 0, + new_authority_signer: None, + }, ], sign_only: false, dump_transaction_message: false, @@ -657,7 +679,12 @@ fn test_stake_authorize() { config.signers.push(&online_authority2); config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, offline_authority_pubkey, 1)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: offline_authority_pubkey, + authority: 1, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::default(), @@ -683,7 +710,12 @@ fn test_stake_authorize() { let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); config_offline.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, nonced_authority_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: nonced_authority_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: true, dump_transaction_message: false, blockhash_query: BlockhashQuery::None(blockhash), @@ -702,7 +734,12 @@ fn test_stake_authorize() { config.signers = vec![&offline_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, nonced_authority_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: nonced_authority_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash), @@ -753,7 +790,12 @@ fn test_stake_authorize() { config_offline.signers.push(&nonced_authority); config_offline.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, online_authority_pubkey, 1)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: online_authority_pubkey, + authority: 1, + new_authority_signer: None, + }], sign_only: true, dump_transaction_message: false, blockhash_query: BlockhashQuery::None(nonce_hash), @@ -773,7 +815,12 @@ fn test_stake_authorize() { config.signers = vec![&offline_presigner, &nonced_authority_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, online_authority_pubkey, 1)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: online_authority_pubkey, + authority: 1, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::FeeCalculator( @@ -861,6 +908,7 @@ fn test_stake_authorize_with_fee_payer() { seed: None, staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: false, @@ -880,7 +928,12 @@ fn test_stake_authorize_with_fee_payer() { config.signers = vec![&default_signer, &payer_keypair]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, offline_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: offline_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), @@ -902,7 +955,12 @@ fn test_stake_authorize_with_fee_payer() { let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); config_offline.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, payer_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: payer_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: true, dump_transaction_message: false, blockhash_query: BlockhashQuery::None(blockhash), @@ -921,7 +979,12 @@ fn test_stake_authorize_with_fee_payer() { config.signers = vec![&offline_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, - new_authorizations: vec![(StakeAuthorize::Staker, payer_pubkey, 0)], + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: payer_pubkey, + authority: 0, + new_authority_signer: None, + }], sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash), @@ -985,6 +1048,7 @@ fn test_stake_split() { seed: None, staker: Some(offline_pubkey), withdrawer: Some(offline_pubkey), + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(10 * minimum_stake_balance), sign_only: false, @@ -1135,6 +1199,7 @@ fn test_stake_set_lockup() { seed: None, staker: Some(offline_pubkey), withdrawer: Some(config.signers[0].pubkey()), + withdrawer_signer: None, lockup, amount: SpendAmount::Some(10 * minimum_stake_balance), sign_only: false, @@ -1163,6 +1228,7 @@ fn test_stake_set_lockup() { config.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, + new_custodian_signer: None, custodian: 0, sign_only: false, dump_transaction_message: false, @@ -1198,6 +1264,7 @@ fn test_stake_set_lockup() { config.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, + new_custodian_signer: None, custodian: 0, sign_only: false, dump_transaction_message: false, @@ -1218,6 +1285,7 @@ fn test_stake_set_lockup() { config.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, + new_custodian_signer: None, custodian: 1, sign_only: false, dump_transaction_message: false, @@ -1250,6 +1318,7 @@ fn test_stake_set_lockup() { config.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, + new_custodian_signer: None, custodian: 1, sign_only: false, dump_transaction_message: false, @@ -1297,6 +1366,7 @@ fn test_stake_set_lockup() { config_offline.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, + new_custodian_signer: None, custodian: 0, sign_only: true, dump_transaction_message: false, @@ -1315,6 +1385,7 @@ fn test_stake_set_lockup() { config.command = CliCommand::StakeSetLockup { stake_account_pubkey, lockup, + new_custodian_signer: None, custodian: 0, sign_only: false, dump_transaction_message: false, @@ -1409,6 +1480,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { seed: None, staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: true, @@ -1432,6 +1504,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { seed: None, staker: Some(offline_pubkey), withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: false, @@ -1521,6 +1594,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { seed: Some(seed.to_string()), staker: None, withdrawer: None, + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: true, @@ -1542,6 +1616,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { seed: Some(seed.to_string()), staker: Some(offline_pubkey), withdrawer: Some(offline_pubkey), + withdrawer_signer: None, lockup: Lockup::default(), amount: SpendAmount::Some(50_000), sign_only: false, @@ -1561,3 +1636,228 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { Pubkey::create_with_seed(&stake_pubkey, seed, &stake::program::id()).unwrap(); check_recent_balance(50_000, &rpc_client, &seed_address); } + +#[test] +fn test_stake_checked_instructions() { + solana_logger::setup(); + + 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)); + + let rpc_client = + RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed()); + let default_signer = Keypair::new(); + + let mut config = CliConfig::recent_for_tests(); + config.json_rpc_url = test_validator.rpc_url(); + config.signers = vec![&default_signer]; + + request_and_confirm_airdrop(&rpc_client, &config, &config.signers[0].pubkey(), 100_000) + .unwrap(); + + // Create stake account with withdrawer + let stake_keypair = Keypair::new(); + let stake_account_pubkey = stake_keypair.pubkey(); + let withdrawer_keypair = Keypair::new(); + let withdrawer_pubkey = withdrawer_keypair.pubkey(); + config.signers.push(&stake_keypair); + config.command = CliCommand::CreateStakeAccount { + stake_account: 1, + seed: None, + staker: None, + withdrawer: Some(withdrawer_pubkey), + withdrawer_signer: Some(1), + lockup: Lockup::default(), + amount: SpendAmount::Some(50_000), + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + from: 0, + }; + process_command(&config).unwrap_err(); // unsigned authority should fail + + config.signers = vec![&default_signer, &stake_keypair, &withdrawer_keypair]; + config.command = CliCommand::CreateStakeAccount { + stake_account: 1, + seed: None, + staker: None, + withdrawer: Some(withdrawer_pubkey), + withdrawer_signer: Some(1), + lockup: Lockup::default(), + amount: SpendAmount::Some(50_000), + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + from: 0, + }; + process_command(&config).unwrap(); + + // Re-authorize account, checking new authority + let staker_keypair = Keypair::new(); + let staker_pubkey = staker_keypair.pubkey(); + config.signers = vec![&default_signer]; + config.command = CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: staker_pubkey, + authority: 0, + new_authority_signer: Some(0), + }], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }; + process_command(&config).unwrap_err(); // unsigned authority should fail + + config.signers = vec![&default_signer, &staker_keypair]; + config.command = CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Staker, + new_authority_pubkey: staker_pubkey, + authority: 0, + new_authority_signer: Some(1), + }], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }; + 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_authority = match stake_state { + StakeState::Initialized(meta) => meta.authorized.staker, + _ => panic!("Unexpected stake state!"), + }; + assert_eq!(current_authority, staker_pubkey); + + let new_withdrawer_keypair = Keypair::new(); + let new_withdrawer_pubkey = new_withdrawer_keypair.pubkey(); + config.signers = vec![&default_signer, &withdrawer_keypair]; + config.command = CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: new_withdrawer_pubkey, + authority: 1, + new_authority_signer: Some(1), + }], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }; + process_command(&config).unwrap_err(); // unsigned authority should fail + + config.signers = vec![ + &default_signer, + &withdrawer_keypair, + &new_withdrawer_keypair, + ]; + config.command = CliCommand::StakeAuthorize { + stake_account_pubkey, + new_authorizations: vec![StakeAuthorizationIndexed { + authorization_type: StakeAuthorize::Withdrawer, + new_authority_pubkey: new_withdrawer_pubkey, + authority: 1, + new_authority_signer: Some(2), + }], + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + custodian: None, + no_wait: false, + }; + 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_authority = match stake_state { + StakeState::Initialized(meta) => meta.authorized.withdrawer, + _ => panic!("Unexpected stake state!"), + }; + assert_eq!(current_authority, new_withdrawer_pubkey); + + // Set lockup, checking new custodian + let custodian = Keypair::new(); + let custodian_pubkey = custodian.pubkey(); + let lockup = LockupArgs { + unix_timestamp: Some(1_581_534_570), + epoch: Some(200), + custodian: Some(custodian_pubkey), + }; + config.signers = vec![&default_signer, &new_withdrawer_keypair]; + config.command = CliCommand::StakeSetLockup { + stake_account_pubkey, + lockup, + new_custodian_signer: Some(1), + custodian: 1, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + }; + process_command(&config).unwrap_err(); // unsigned new custodian should fail + + config.signers = vec![&default_signer, &new_withdrawer_keypair, &custodian]; + config.command = CliCommand::StakeSetLockup { + stake_account_pubkey, + lockup, + new_custodian_signer: Some(2), + custodian: 1, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + }; + 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.unix_timestamp, + lockup.unix_timestamp.unwrap() + ); + assert_eq!(current_lockup.epoch, lockup.epoch.unwrap()); + assert_eq!(current_lockup.custodian, custodian_pubkey); +} diff --git a/cli/tests/vote.rs b/cli/tests/vote.rs index 64c67a1d0b..532fc19e9c 100644 --- a/cli/tests/vote.rs +++ b/cli/tests/vote.rs @@ -83,13 +83,48 @@ fn test_vote_authorize_and_withdraw() { check_recent_balance(expected_balance, &rpc_client, &vote_account_pubkey); // Authorize vote account withdrawal to another signer - let withdraw_authority = Keypair::new(); + let first_withdraw_authority = Keypair::new(); config.signers = vec![&default_signer]; + config.command = CliCommand::VoteAuthorize { + vote_account_pubkey, + new_authorized_pubkey: first_withdraw_authority.pubkey(), + vote_authorize: VoteAuthorize::Withdrawer, + memo: None, + authorized: 0, + new_authorized: None, + }; + process_command(&config).unwrap(); + let vote_account = rpc_client + .get_account(&vote_account_keypair.pubkey()) + .unwrap(); + let vote_state: VoteStateVersions = vote_account.state().unwrap(); + let authorized_withdrawer = vote_state.convert_to_current().authorized_withdrawer; + assert_eq!(authorized_withdrawer, first_withdraw_authority.pubkey()); + + // Authorize vote account withdrawal to another signer with checked instruction + let withdraw_authority = Keypair::new(); + config.signers = vec![&default_signer, &first_withdraw_authority]; config.command = CliCommand::VoteAuthorize { vote_account_pubkey, new_authorized_pubkey: withdraw_authority.pubkey(), vote_authorize: VoteAuthorize::Withdrawer, memo: None, + authorized: 1, + new_authorized: Some(1), + }; + process_command(&config).unwrap_err(); // unsigned by new authority should fail + config.signers = vec![ + &default_signer, + &first_withdraw_authority, + &withdraw_authority, + ]; + config.command = CliCommand::VoteAuthorize { + vote_account_pubkey, + new_authorized_pubkey: withdraw_authority.pubkey(), + vote_authorize: VoteAuthorize::Withdrawer, + memo: None, + authorized: 1, + new_authorized: Some(2), }; process_command(&config).unwrap(); let vote_account = rpc_client