From 2ea88b41fb05b3f485f33a8e58128fb853a9adc5 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Fri, 11 Aug 2023 13:24:14 -0600 Subject: [PATCH] cli: skip delegate-stake current voter check for unstaked voters (#32787) * cli: use `getVoteAccounts` for delegate-stake current voter check * cli: skip delegate-stake current voter check for unstaked voters * cli: refactor delegate-stake current voter check --- cli/src/stake.rs | 68 ++++++++++++++++++++++-------------------- cli/tests/stake.rs | 74 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 104 insertions(+), 38 deletions(-) diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 2c479950d..de14ec87c 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -32,7 +32,9 @@ use { solana_remote_wallet::remote_wallet::RemoteWalletManager, solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_api::{ - request::DELINQUENT_VALIDATOR_SLOT_DISTANCE, response::RpcInflationReward, + config::RpcGetVoteAccountsConfig, + request::DELINQUENT_VALIDATOR_SLOT_DISTANCE, + response::{RpcInflationReward, RpcVoteAccountStatus}, }, solana_rpc_client_nonce_utils::blockhash_query::BlockhashQuery, solana_sdk::{ @@ -57,7 +59,6 @@ use { sysvar::{clock, stake_history}, transaction::Transaction, }, - solana_vote_program::vote_state::VoteState, std::{ops::Deref, sync::Arc}, }; @@ -2543,38 +2544,41 @@ pub fn process_delegate_stake( if !sign_only { // Sanity check the vote account to ensure it is attached to a validator that has recently // voted at the tip of the ledger - let vote_account_data = rpc_client - .get_account(vote_account_pubkey) - .map_err(|err| { - CliError::RpcRequestError(format!( - "Vote account not found: {vote_account_pubkey}. error: {err}", - )) - })? - .data; + let get_vote_accounts_config = RpcGetVoteAccountsConfig { + vote_pubkey: Some(vote_account_pubkey.to_string()), + keep_unstaked_delinquents: Some(true), + commitment: Some(rpc_client.commitment()), + ..RpcGetVoteAccountsConfig::default() + }; + let RpcVoteAccountStatus { + current, + delinquent, + } = rpc_client.get_vote_accounts_with_config(get_vote_accounts_config)?; + // filter should return at most one result + let rpc_vote_account = + current + .get(0) + .or_else(|| delinquent.get(0)) + .ok_or(CliError::RpcRequestError(format!( + "Vote account not found: {vote_account_pubkey}" + )))?; - let vote_state = VoteState::deserialize(&vote_account_data).map_err(|_| { - CliError::RpcRequestError( - "Account data could not be deserialized to vote state".to_string(), - ) - })?; - - let sanity_check_result = match vote_state.root_slot { - None => Err(CliError::BadParameter( + let activated_stake = rpc_vote_account.activated_stake; + let root_slot = rpc_vote_account.root_slot; + let min_root_slot = rpc_client + .get_slot() + .map(|slot| slot.saturating_sub(DELINQUENT_VALIDATOR_SLOT_DISTANCE))?; + let sanity_check_result = if root_slot >= min_root_slot || activated_stake == 0 { + Ok(()) + } else if root_slot == 0 { + Err(CliError::BadParameter( "Unable to delegate. Vote account has no root slot".to_string(), - )), - Some(root_slot) => { - let min_root_slot = rpc_client - .get_slot()? - .saturating_sub(DELINQUENT_VALIDATOR_SLOT_DISTANCE); - if root_slot < min_root_slot { - Err(CliError::DynamicProgramError(format!( - "Unable to delegate. Vote account appears delinquent \ - because its current root slot, {root_slot}, is less than {min_root_slot}" - ))) - } else { - Ok(()) - } - } + )) + } else { + Err(CliError::DynamicProgramError(format!( + "Unable to delegate. Vote account appears delinquent \ + because its current root slot, {root_slot}, is less than {min_root_slot}" + ))) }; if let Err(err) = &sanity_check_result { diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 0af894432..eb4bac8f9 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -11,7 +11,10 @@ use { solana_cli_output::{parse_sign_only_reply_string, OutputFormat}, solana_faucet::faucet::run_local_faucet, solana_rpc_client::rpc_client::RpcClient, - solana_rpc_client_api::response::{RpcStakeActivation, StakeActivationState}, + solana_rpc_client_api::{ + request::DELINQUENT_VALIDATOR_SLOT_DISTANCE, + response::{RpcStakeActivation, StakeActivationState}, + }, solana_rpc_client_nonce_utils::blockhash_query::{self, BlockhashQuery}, solana_sdk::{ account_utils::StateMut, @@ -295,8 +298,23 @@ fn test_stake_delegation_force() { let mint_pubkey = mint_keypair.pubkey(); let authorized_withdrawer = Keypair::new().pubkey(); let faucet_addr = run_local_faucet(mint_keypair, None); - let test_validator = - TestValidator::with_no_fees(mint_pubkey, Some(faucet_addr), SocketAddrSpace::Unspecified); + let slots_per_epoch = 32; + let test_validator = TestValidatorGenesis::default() + .fee_rate_governor(FeeRateGovernor::new(0, 0)) + .rent(Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }) + .epoch_schedule(EpochSchedule::custom( + slots_per_epoch, + slots_per_epoch, + /* enable_warmup_epochs = */ false, + )) + .faucet_addr(Some(faucet_addr)) + .warp_slot(DELINQUENT_VALIDATOR_SLOT_DISTANCE * 2) // get out in front of the cli voter delinquency check + .start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified) + .expect("validator start failed"); let rpc_client = RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed()); @@ -345,7 +363,7 @@ fn test_stake_delegation_force() { withdrawer: None, withdrawer_signer: None, lockup: Lockup::default(), - amount: SpendAmount::Some(50_000_000_000), + amount: SpendAmount::Some(25_000_000_000), sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), @@ -358,7 +376,7 @@ fn test_stake_delegation_force() { }; process_command(&config).unwrap(); - // Delegate stake fails (vote account had never voted) + // Delegate stake succeeds despite no votes, because voter has zero stake config.signers = vec![&default_signer]; config.command = CliCommand::DelegateStake { stake_account_pubkey: stake_keypair.pubkey(), @@ -375,11 +393,55 @@ fn test_stake_delegation_force() { redelegation_stake_account: None, compute_unit_price: None, }; + process_command(&config).unwrap(); + + // Create a second stake account + let stake_keypair2 = Keypair::new(); + config.signers = vec![&default_signer, &stake_keypair2]; + config.command = CliCommand::CreateStakeAccount { + stake_account: 1, + seed: None, + staker: None, + withdrawer: None, + withdrawer_signer: None, + lockup: Lockup::default(), + amount: SpendAmount::Some(25_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, + compute_unit_price: None, + }; + process_command(&config).unwrap(); + + wait_for_next_epoch_plus_n_slots(&rpc_client, 1); + + // Delegate stake2 fails because voter has not voted, but is now staked + config.signers = vec![&default_signer]; + config.command = CliCommand::DelegateStake { + stake_account_pubkey: stake_keypair2.pubkey(), + vote_account_pubkey: vote_keypair.pubkey(), + stake_authority: 0, + force: false, + sign_only: false, + dump_transaction_message: false, + blockhash_query: BlockhashQuery::default(), + nonce_account: None, + nonce_authority: 0, + memo: None, + fee_payer: 0, + redelegation_stake_account: None, + compute_unit_price: None, + }; process_command(&config).unwrap_err(); // But if we force it, it works anyway! config.command = CliCommand::DelegateStake { - stake_account_pubkey: stake_keypair.pubkey(), + stake_account_pubkey: stake_keypair2.pubkey(), vote_account_pubkey: vote_keypair.pubkey(), stake_authority: 0, force: true,