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
This commit is contained in:
Trent Nelson 2023-08-11 13:24:14 -06:00 committed by GitHub
parent a1a0829a8b
commit 2ea88b41fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 104 additions and 38 deletions

View File

@ -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 {

View File

@ -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,