Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.

Commit 2ea88b4

Browse files
authored
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
1 parent a1a0829 commit 2ea88b4

File tree

2 files changed

+105
-39
lines changed

2 files changed

+105
-39
lines changed

cli/src/stake.rs

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ use {
3232
solana_remote_wallet::remote_wallet::RemoteWalletManager,
3333
solana_rpc_client::rpc_client::RpcClient,
3434
solana_rpc_client_api::{
35-
request::DELINQUENT_VALIDATOR_SLOT_DISTANCE, response::RpcInflationReward,
35+
config::RpcGetVoteAccountsConfig,
36+
request::DELINQUENT_VALIDATOR_SLOT_DISTANCE,
37+
response::{RpcInflationReward, RpcVoteAccountStatus},
3638
},
3739
solana_rpc_client_nonce_utils::blockhash_query::BlockhashQuery,
3840
solana_sdk::{
@@ -57,7 +59,6 @@ use {
5759
sysvar::{clock, stake_history},
5860
transaction::Transaction,
5961
},
60-
solana_vote_program::vote_state::VoteState,
6162
std::{ops::Deref, sync::Arc},
6263
};
6364

@@ -2543,38 +2544,41 @@ pub fn process_delegate_stake(
25432544
if !sign_only {
25442545
// Sanity check the vote account to ensure it is attached to a validator that has recently
25452546
// voted at the tip of the ledger
2546-
let vote_account_data = rpc_client
2547-
.get_account(vote_account_pubkey)
2548-
.map_err(|err| {
2549-
CliError::RpcRequestError(format!(
2550-
"Vote account not found: {vote_account_pubkey}. error: {err}",
2551-
))
2552-
})?
2553-
.data;
2554-
2555-
let vote_state = VoteState::deserialize(&vote_account_data).map_err(|_| {
2556-
CliError::RpcRequestError(
2557-
"Account data could not be deserialized to vote state".to_string(),
2558-
)
2559-
})?;
2560-
2561-
let sanity_check_result = match vote_state.root_slot {
2562-
None => Err(CliError::BadParameter(
2547+
let get_vote_accounts_config = RpcGetVoteAccountsConfig {
2548+
vote_pubkey: Some(vote_account_pubkey.to_string()),
2549+
keep_unstaked_delinquents: Some(true),
2550+
commitment: Some(rpc_client.commitment()),
2551+
..RpcGetVoteAccountsConfig::default()
2552+
};
2553+
let RpcVoteAccountStatus {
2554+
current,
2555+
delinquent,
2556+
} = rpc_client.get_vote_accounts_with_config(get_vote_accounts_config)?;
2557+
// filter should return at most one result
2558+
let rpc_vote_account =
2559+
current
2560+
.get(0)
2561+
.or_else(|| delinquent.get(0))
2562+
.ok_or(CliError::RpcRequestError(format!(
2563+
"Vote account not found: {vote_account_pubkey}"
2564+
)))?;
2565+
2566+
let activated_stake = rpc_vote_account.activated_stake;
2567+
let root_slot = rpc_vote_account.root_slot;
2568+
let min_root_slot = rpc_client
2569+
.get_slot()
2570+
.map(|slot| slot.saturating_sub(DELINQUENT_VALIDATOR_SLOT_DISTANCE))?;
2571+
let sanity_check_result = if root_slot >= min_root_slot || activated_stake == 0 {
2572+
Ok(())
2573+
} else if root_slot == 0 {
2574+
Err(CliError::BadParameter(
25632575
"Unable to delegate. Vote account has no root slot".to_string(),
2564-
)),
2565-
Some(root_slot) => {
2566-
let min_root_slot = rpc_client
2567-
.get_slot()?
2568-
.saturating_sub(DELINQUENT_VALIDATOR_SLOT_DISTANCE);
2569-
if root_slot < min_root_slot {
2570-
Err(CliError::DynamicProgramError(format!(
2571-
"Unable to delegate. Vote account appears delinquent \
2572-
because its current root slot, {root_slot}, is less than {min_root_slot}"
2573-
)))
2574-
} else {
2575-
Ok(())
2576-
}
2577-
}
2576+
))
2577+
} else {
2578+
Err(CliError::DynamicProgramError(format!(
2579+
"Unable to delegate. Vote account appears delinquent \
2580+
because its current root slot, {root_slot}, is less than {min_root_slot}"
2581+
)))
25782582
};
25792583

25802584
if let Err(err) = &sanity_check_result {

cli/tests/stake.rs

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ use {
1111
solana_cli_output::{parse_sign_only_reply_string, OutputFormat},
1212
solana_faucet::faucet::run_local_faucet,
1313
solana_rpc_client::rpc_client::RpcClient,
14-
solana_rpc_client_api::response::{RpcStakeActivation, StakeActivationState},
14+
solana_rpc_client_api::{
15+
request::DELINQUENT_VALIDATOR_SLOT_DISTANCE,
16+
response::{RpcStakeActivation, StakeActivationState},
17+
},
1518
solana_rpc_client_nonce_utils::blockhash_query::{self, BlockhashQuery},
1619
solana_sdk::{
1720
account_utils::StateMut,
@@ -295,8 +298,23 @@ fn test_stake_delegation_force() {
295298
let mint_pubkey = mint_keypair.pubkey();
296299
let authorized_withdrawer = Keypair::new().pubkey();
297300
let faucet_addr = run_local_faucet(mint_keypair, None);
298-
let test_validator =
299-
TestValidator::with_no_fees(mint_pubkey, Some(faucet_addr), SocketAddrSpace::Unspecified);
301+
let slots_per_epoch = 32;
302+
let test_validator = TestValidatorGenesis::default()
303+
.fee_rate_governor(FeeRateGovernor::new(0, 0))
304+
.rent(Rent {
305+
lamports_per_byte_year: 1,
306+
exemption_threshold: 1.0,
307+
..Rent::default()
308+
})
309+
.epoch_schedule(EpochSchedule::custom(
310+
slots_per_epoch,
311+
slots_per_epoch,
312+
/* enable_warmup_epochs = */ false,
313+
))
314+
.faucet_addr(Some(faucet_addr))
315+
.warp_slot(DELINQUENT_VALIDATOR_SLOT_DISTANCE * 2) // get out in front of the cli voter delinquency check
316+
.start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified)
317+
.expect("validator start failed");
300318

301319
let rpc_client =
302320
RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed());
@@ -345,7 +363,7 @@ fn test_stake_delegation_force() {
345363
withdrawer: None,
346364
withdrawer_signer: None,
347365
lockup: Lockup::default(),
348-
amount: SpendAmount::Some(50_000_000_000),
366+
amount: SpendAmount::Some(25_000_000_000),
349367
sign_only: false,
350368
dump_transaction_message: false,
351369
blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
@@ -358,7 +376,7 @@ fn test_stake_delegation_force() {
358376
};
359377
process_command(&config).unwrap();
360378

361-
// Delegate stake fails (vote account had never voted)
379+
// Delegate stake succeeds despite no votes, because voter has zero stake
362380
config.signers = vec![&default_signer];
363381
config.command = CliCommand::DelegateStake {
364382
stake_account_pubkey: stake_keypair.pubkey(),
@@ -375,11 +393,55 @@ fn test_stake_delegation_force() {
375393
redelegation_stake_account: None,
376394
compute_unit_price: None,
377395
};
396+
process_command(&config).unwrap();
397+
398+
// Create a second stake account
399+
let stake_keypair2 = Keypair::new();
400+
config.signers = vec![&default_signer, &stake_keypair2];
401+
config.command = CliCommand::CreateStakeAccount {
402+
stake_account: 1,
403+
seed: None,
404+
staker: None,
405+
withdrawer: None,
406+
withdrawer_signer: None,
407+
lockup: Lockup::default(),
408+
amount: SpendAmount::Some(25_000_000_000),
409+
sign_only: false,
410+
dump_transaction_message: false,
411+
blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
412+
nonce_account: None,
413+
nonce_authority: 0,
414+
memo: None,
415+
fee_payer: 0,
416+
from: 0,
417+
compute_unit_price: None,
418+
};
419+
process_command(&config).unwrap();
420+
421+
wait_for_next_epoch_plus_n_slots(&rpc_client, 1);
422+
423+
// Delegate stake2 fails because voter has not voted, but is now staked
424+
config.signers = vec![&default_signer];
425+
config.command = CliCommand::DelegateStake {
426+
stake_account_pubkey: stake_keypair2.pubkey(),
427+
vote_account_pubkey: vote_keypair.pubkey(),
428+
stake_authority: 0,
429+
force: false,
430+
sign_only: false,
431+
dump_transaction_message: false,
432+
blockhash_query: BlockhashQuery::default(),
433+
nonce_account: None,
434+
nonce_authority: 0,
435+
memo: None,
436+
fee_payer: 0,
437+
redelegation_stake_account: None,
438+
compute_unit_price: None,
439+
};
378440
process_command(&config).unwrap_err();
379441

380442
// But if we force it, it works anyway!
381443
config.command = CliCommand::DelegateStake {
382-
stake_account_pubkey: stake_keypair.pubkey(),
444+
stake_account_pubkey: stake_keypair2.pubkey(),
383445
vote_account_pubkey: vote_keypair.pubkey(),
384446
stake_authority: 0,
385447
force: true,

0 commit comments

Comments
 (0)