Add StakeInstruction::DeactivateDelinquent

This commit is contained in:
Michael Vines 2022-03-25 09:11:51 -07:00
parent b9caa8cdfb
commit 57ff7371b4
12 changed files with 715 additions and 17 deletions

View File

@ -188,6 +188,7 @@ pub enum CliCommand {
stake_account_pubkey: Pubkey,
stake_authority: SignerIndex,
sign_only: bool,
deactivate_delinquent: bool,
dump_transaction_message: bool,
blockhash_query: BlockhashQuery,
nonce_account: Option<Pubkey>,
@ -1083,6 +1084,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
stake_account_pubkey,
stake_authority,
sign_only,
deactivate_delinquent,
dump_transaction_message,
blockhash_query,
nonce_account,
@ -1096,6 +1098,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
stake_account_pubkey,
*stake_authority,
*sign_only,
*deactivate_delinquent,
*dump_transaction_message,
blockhash_query,
*nonce_account,
@ -2092,6 +2095,7 @@ mod tests {
stake_authority: 0,
sign_only: false,
dump_transaction_message: false,
deactivate_delinquent: false,
blockhash_query: BlockhashQuery::default(),
nonce_account: None,
nonce_authority: 0,

View File

@ -41,6 +41,7 @@ use {
self,
instruction::{self as stake_instruction, LockupArgs, StakeError},
state::{Authorized, Lockup, Meta, StakeActivationStatus, StakeAuthorize, StakeState},
tools::{acceptable_reference_epoch_credits, eligible_for_deactivate_delinquent},
},
stake_history::StakeHistory,
system_instruction::SystemError,
@ -379,6 +380,13 @@ impl StakeSubCommands for App<'_, '_> {
.help("Seed for address generation; if specified, the resulting account \
will be at a derived address of STAKE_ACCOUNT_ADDRESS")
)
.arg(
Arg::with_name("delinquent")
.long("delinquent")
.takes_value(false)
.conflicts_with(SIGN_ONLY_ARG.name)
.help("Deactivate abandoned stake that is currently delegated to a delinquent vote account")
)
.arg(stake_authority_arg())
.offline_args()
.nonce_args(false)
@ -995,11 +1003,13 @@ pub fn parse_stake_deactivate_stake(
let stake_account_pubkey =
pubkey_of_signer(matches, "stake_account_pubkey", wallet_manager)?.unwrap();
let sign_only = matches.is_present(SIGN_ONLY_ARG.name);
let deactivate_delinquent = matches.is_present("delinquent");
let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name);
let blockhash_query = BlockhashQuery::new_from_matches(matches);
let nonce_account = pubkey_of(matches, NONCE_ARG.name);
let memo = matches.value_of(MEMO_ARG.name).map(String::from);
let seed = value_t!(matches, "seed", String).ok();
let (stake_authority, stake_authority_pubkey) =
signer_of(matches, STAKE_AUTHORITY_ARG.name, wallet_manager)?;
let (nonce_authority, nonce_authority_pubkey) =
@ -1018,6 +1028,7 @@ pub fn parse_stake_deactivate_stake(
stake_account_pubkey,
stake_authority: signer_info.index_of(stake_authority_pubkey).unwrap(),
sign_only,
deactivate_delinquent,
dump_transaction_message,
blockhash_query,
nonce_account,
@ -1477,6 +1488,7 @@ pub fn process_deactivate_stake_account(
stake_account_pubkey: &Pubkey,
stake_authority: SignerIndex,
sign_only: bool,
deactivate_delinquent: bool,
dump_transaction_message: bool,
blockhash_query: &BlockhashQuery,
nonce_account: Option<Pubkey>,
@ -1486,7 +1498,6 @@ pub fn process_deactivate_stake_account(
fee_payer: SignerIndex,
) -> ProcessResult {
let recent_blockhash = blockhash_query.get_blockhash(rpc_client, config.commitment)?;
let stake_authority = config.signers[stake_authority];
let stake_account_address = if let Some(seed) = seed {
Pubkey::create_with_seed(stake_account_pubkey, seed, &stake::program::id())?
@ -1494,11 +1505,77 @@ pub fn process_deactivate_stake_account(
*stake_account_pubkey
};
let ixs = vec![stake_instruction::deactivate_stake(
&stake_account_address,
&stake_authority.pubkey(),
)]
let ixs = vec![if deactivate_delinquent {
let stake_account = rpc_client.get_account(&stake_account_address)?;
if stake_account.owner != stake::program::id() {
return Err(CliError::BadParameter(format!(
"{} is not a stake account",
stake_account_address,
))
.into());
}
let vote_account_address = match stake_account.state() {
Ok(stake_state) => match stake_state {
StakeState::Stake(_, stake) => stake.delegation.voter_pubkey,
_ => {
return Err(CliError::BadParameter(format!(
"{} is not a delegated stake account",
stake_account_address,
))
.into())
}
},
Err(err) => {
return Err(CliError::RpcRequestError(format!(
"Account data could not be deserialized to stake state: {}",
err
))
.into())
}
};
let current_epoch = rpc_client.get_epoch_info()?.epoch;
let (_, vote_state) = crate::vote::get_vote_account(
rpc_client,
&vote_account_address,
rpc_client.commitment(),
)?;
if !eligible_for_deactivate_delinquent(&vote_state.epoch_credits, current_epoch) {
return Err(CliError::BadParameter(format!(
"Stake has not been delinquent for {} epochs",
stake::MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION,
))
.into());
}
// Search for a reference vote account
let reference_vote_account_address = rpc_client
.get_vote_accounts()?
.current
.into_iter()
.find(|vote_account_info| {
acceptable_reference_epoch_credits(&vote_account_info.epoch_credits, current_epoch)
});
let reference_vote_account_address = reference_vote_account_address
.ok_or_else(|| {
CliError::RpcRequestError("Unable to find a reference vote account".into())
})?
.vote_pubkey
.parse()?;
stake_instruction::deactivate_delinquent_stake(
&stake_account_address,
&vote_account_address,
&reference_vote_account_address,
)
} else {
let stake_authority = config.signers[stake_authority];
stake_instruction::deactivate_stake(&stake_account_address, &stake_authority.pubkey())
}]
.with_memo(memo);
let nonce_authority = config.signers[nonce_authority];
let fee_payer = config.signers[fee_payer];
@ -4174,6 +4251,34 @@ mod tests {
stake_account_pubkey,
stake_authority: 0,
sign_only: false,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::default(),
nonce_account: None,
nonce_authority: 0,
memo: None,
seed: None,
fee_payer: 0,
},
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
}
);
// Test DeactivateStake Subcommand with delinquent flag
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
"test",
"deactivate-stake",
&stake_account_string,
"--delinquent",
]);
assert_eq!(
parse_command(&test_deactivate_stake, &default_signer, &mut None).unwrap(),
CliCommandInfo {
command: CliCommand::DeactivateStake {
stake_account_pubkey,
stake_authority: 0,
sign_only: false,
deactivate_delinquent: true,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::default(),
nonce_account: None,
@ -4201,6 +4306,7 @@ mod tests {
stake_account_pubkey,
stake_authority: 1,
sign_only: false,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::default(),
nonce_account: None,
@ -4235,6 +4341,7 @@ mod tests {
stake_account_pubkey,
stake_authority: 0,
sign_only: false,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::FeeCalculator(
blockhash_query::Source::Cluster,
@ -4265,6 +4372,7 @@ mod tests {
stake_account_pubkey,
stake_authority: 0,
sign_only: true,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::None(blockhash),
nonce_account: None,
@ -4299,6 +4407,7 @@ mod tests {
stake_account_pubkey,
stake_authority: 0,
sign_only: false,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::FeeCalculator(
blockhash_query::Source::Cluster,
@ -4345,6 +4454,7 @@ mod tests {
stake_account_pubkey,
stake_authority: 0,
sign_only: false,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::FeeCalculator(
blockhash_query::Source::NonceAccount(nonce_account),
@ -4379,6 +4489,7 @@ mod tests {
stake_account_pubkey,
stake_authority: 0,
sign_only: false,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
nonce_account: None,

View File

@ -1140,7 +1140,7 @@ pub fn process_vote_update_commission(
}
}
fn get_vote_account(
pub(crate) fn get_vote_account(
rpc_client: &RpcClient,
vote_account_pubkey: &Pubkey,
commitment_config: CommitmentConfig,

View File

@ -204,6 +204,7 @@ fn test_seed_stake_delegation_and_deactivation() {
stake_account_pubkey: stake_address,
stake_authority: 0,
sign_only: false,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::default(),
nonce_account: None,
@ -287,6 +288,7 @@ fn test_stake_delegation_and_deactivation() {
stake_account_pubkey: stake_keypair.pubkey(),
stake_authority: 0,
sign_only: false,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::default(),
nonce_account: None,
@ -412,6 +414,7 @@ fn test_offline_stake_delegation_and_deactivation() {
stake_account_pubkey: stake_keypair.pubkey(),
stake_authority: 0,
sign_only: true,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::None(blockhash),
nonce_account: None,
@ -431,6 +434,7 @@ fn test_offline_stake_delegation_and_deactivation() {
stake_account_pubkey: stake_keypair.pubkey(),
stake_authority: 0,
sign_only: false,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
nonce_account: None,
@ -546,6 +550,7 @@ fn test_nonced_stake_delegation_and_deactivation() {
stake_account_pubkey: stake_keypair.pubkey(),
stake_authority: 0,
sign_only: false,
deactivate_delinquent: false,
dump_transaction_message: false,
blockhash_query: BlockhashQuery::FeeCalculator(
blockhash_query::Source::NonceAccount(nonce_account.pubkey()),

View File

@ -2,8 +2,8 @@ use {
crate::{
config,
stake_state::{
authorize, authorize_with_seed, deactivate, delegate, initialize, merge, set_lockup,
split, withdraw,
authorize, authorize_with_seed, deactivate, deactivate_delinquent, delegate,
initialize, merge, set_lockup, split, withdraw,
},
},
log::*,
@ -416,6 +416,27 @@ pub fn process_instruction(
.transaction_context
.set_return_data(id(), minimum_delegation)
}
Ok(StakeInstruction::DeactivateDelinquent) => {
let mut me = get_stake_account()?;
if invoke_context
.feature_set
.is_active(&feature_set::stake_deactivate_delinquent_instruction::id())
{
instruction_context.check_number_of_instruction_accounts(3)?;
let clock = invoke_context.get_sysvar_cache().get_clock()?;
deactivate_delinquent(
transaction_context,
instruction_context,
&mut me,
first_instruction_account + 1,
first_instruction_account + 2,
clock.epoch,
)
} else {
Err(InstructionError::InvalidInstructionData)
}
}
Err(err) => {
if !invoke_context.feature_set.is_active(
&feature_set::add_get_minimum_delegation_instruction_to_stake_program::id(),
@ -432,8 +453,8 @@ mod tests {
use {
super::*,
crate::stake_state::{
authorized_from, create_stake_history_from_delegations, from, stake_from, Delegation,
Meta, Stake, StakeState,
authorized_from, create_stake_history_from_delegations, from, new_stake, stake_from,
Delegation, Meta, Stake, StakeState,
},
bincode::serialize,
solana_program_runtime::{
@ -455,12 +476,13 @@ mod tests {
LockupArgs, StakeError,
},
state::{Authorized, Lockup, StakeAuthorize},
MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION,
},
stake_history::{StakeHistory, StakeHistoryEntry},
system_program, sysvar,
},
solana_vote_program::vote_state::{self, VoteState, VoteStateVersions},
std::{collections::HashSet, str::FromStr, sync::Arc},
std::{borrow::BorrowMut, collections::HashSet, str::FromStr, sync::Arc},
};
fn create_default_account() -> AccountSharedData {
@ -661,6 +683,30 @@ mod tests {
),
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
&instruction::deactivate_delinquent_stake(
&Pubkey::new_unique(),
&Pubkey::new_unique(),
&invalid_vote_state_pubkey(),
),
Err(InstructionError::IncorrectProgramId),
);
process_instruction_as_one_arg(
&instruction::deactivate_delinquent_stake(
&Pubkey::new_unique(),
&invalid_vote_state_pubkey(),
&Pubkey::new_unique(),
),
Err(InstructionError::InvalidAccountData),
);
process_instruction_as_one_arg(
&instruction::deactivate_delinquent_stake(
&Pubkey::new_unique(),
&invalid_vote_state_pubkey(),
&invalid_vote_state_pubkey(),
),
Err(InstructionError::InvalidAccountData),
);
}
#[test]
@ -758,6 +804,14 @@ mod tests {
),
Err(InstructionError::InvalidAccountOwner),
);
process_instruction_as_one_arg(
&instruction::deactivate_delinquent_stake(
&spoofed_stake_state_pubkey(),
&Pubkey::new_unique(),
&Pubkey::new_unique(),
),
Err(InstructionError::InvalidAccountOwner),
);
}
#[test]
@ -908,7 +962,7 @@ mod tests {
&serialize(&StakeInstruction::Withdraw(withdrawal_amount)).unwrap(),
vec![
(stake_address, stake_account.clone()),
(vote_address, vote_account),
(vote_address, vote_account.clone()),
(rewards_address, rewards_account.clone()),
(stake_history_address, stake_history_account),
],
@ -953,7 +1007,7 @@ mod tests {
process_instruction(
&serialize(&StakeInstruction::Deactivate).unwrap(),
vec![
(stake_address, stake_account),
(stake_address, stake_account.clone()),
(rewards_address, rewards_account),
],
vec![
@ -978,6 +1032,41 @@ mod tests {
Vec::new(),
Err(InstructionError::NotEnoughAccountKeys),
);
// Tests correct number of accounts are provided in deactivate_delinquent
process_instruction(
&serialize(&StakeInstruction::DeactivateDelinquent).unwrap(),
Vec::new(),
Vec::new(),
Err(InstructionError::NotEnoughAccountKeys),
);
process_instruction(
&serialize(&StakeInstruction::DeactivateDelinquent).unwrap(),
vec![(stake_address, stake_account.clone())],
vec![AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: false,
}],
Err(InstructionError::NotEnoughAccountKeys),
);
process_instruction(
&serialize(&StakeInstruction::DeactivateDelinquent).unwrap(),
vec![(stake_address, stake_account), (vote_address, vote_account)],
vec![
AccountMeta {
pubkey: stake_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
],
Err(InstructionError::NotEnoughAccountKeys),
);
}
#[test]
@ -6128,4 +6217,268 @@ mod tests {
);
}
}
#[test]
fn test_deactivate_delinquent() {
let mut sysvar_cache_override = SysvarCache::default();
let reference_vote_address = Pubkey::new_unique();
let vote_address = Pubkey::new_unique();
let stake_address = Pubkey::new_unique();
let initial_stake_state = StakeState::Stake(
Meta::default(),
new_stake(
1, /* stake */
&vote_address,
&VoteState::default(),
1, /* activation_epoch */
&stake_config::Config::default(),
),
);
let stake_account = AccountSharedData::new_data_with_space(
1, /* lamports */
&initial_stake_state,
std::mem::size_of::<StakeState>(),
&id(),
)
.unwrap();
let mut vote_account = AccountSharedData::new_data_with_space(
1, /* lamports */
&VoteStateVersions::new_current(VoteState::default()),
VoteState::size_of(),
&solana_vote_program::id(),
)
.unwrap();
let mut reference_vote_account = AccountSharedData::new_data_with_space(
1, /* lamports */
&VoteStateVersions::new_current(VoteState::default()),
VoteState::size_of(),
&solana_vote_program::id(),
)
.unwrap();
let current_epoch = 20;
sysvar_cache_override.set_clock(Clock {
epoch: current_epoch,
..Clock::default()
});
let process_instruction_deactivate_delinquent =
|stake_address: &Pubkey,
stake_account: &AccountSharedData,
vote_account: &AccountSharedData,
reference_vote_account: &AccountSharedData,
expected_result| {
process_instruction_with_sysvar_cache(
&serialize(&StakeInstruction::DeactivateDelinquent).unwrap(),
vec![
(*stake_address, stake_account.clone()),
(vote_address, vote_account.clone()),
(reference_vote_address, reference_vote_account.clone()),
],
vec![
AccountMeta {
pubkey: *stake_address,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: vote_address,
is_signer: false,
is_writable: false,
},
AccountMeta {
pubkey: reference_vote_address,
is_signer: false,
is_writable: false,
},
],
Some(&sysvar_cache_override),
expected_result,
)
};
// `reference_vote_account` has not voted. Instruction will fail
process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Err(StakeError::InsufficientReferenceVotes.into()),
);
// `reference_vote_account` has not consistently voted for at least
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`.
// Instruction will fail
let mut reference_vote_state = VoteState::default();
for epoch in 0..MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION / 2 {
reference_vote_state.increment_credits(epoch as Epoch);
}
reference_vote_account
.borrow_mut()
.serialize_data(&VoteStateVersions::new_current(reference_vote_state))
.unwrap();
process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Err(StakeError::InsufficientReferenceVotes.into()),
);
// `reference_vote_account` has not consistently voted for the last
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`.
// Instruction will fail
let mut reference_vote_state = VoteState::default();
for epoch in 0..=current_epoch {
reference_vote_state.increment_credits(epoch);
}
assert_eq!(
reference_vote_state.epoch_credits[current_epoch as usize - 2].0,
current_epoch - 2
);
reference_vote_state
.epoch_credits
.remove(current_epoch as usize - 2);
assert_eq!(
reference_vote_state.epoch_credits[current_epoch as usize - 2].0,
current_epoch - 1
);
reference_vote_account
.borrow_mut()
.serialize_data(&VoteStateVersions::new_current(reference_vote_state))
.unwrap();
process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Err(StakeError::InsufficientReferenceVotes.into()),
);
// `reference_vote_account` has consistently voted and `vote_account` has never voted.
// Instruction will succeed
let mut reference_vote_state = VoteState::default();
for epoch in 0..=current_epoch {
reference_vote_state.increment_credits(epoch);
}
reference_vote_account
.borrow_mut()
.serialize_data(&VoteStateVersions::new_current(reference_vote_state))
.unwrap();
let post_stake_account = &process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Ok(()),
)[0];
assert_eq!(
stake_from(post_stake_account)
.unwrap()
.delegation
.deactivation_epoch,
current_epoch
);
// `reference_vote_account` has consistently voted and `vote_account` has not voted for the
// last `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`.
// Instruction will succeed
let mut vote_state = VoteState::default();
for epoch in 0..MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION / 2 {
vote_state.increment_credits(epoch as Epoch);
}
vote_account
.serialize_data(&VoteStateVersions::new_current(vote_state))
.unwrap();
let post_stake_account = &process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Ok(()),
)[0];
assert_eq!(
stake_from(post_stake_account)
.unwrap()
.delegation
.deactivation_epoch,
current_epoch
);
// `reference_vote_account` has consistently voted and `vote_account` has not voted for the
// last `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`. Try to deactivate an unrelated stake
// account. Instruction will fail
let unrelated_vote_address = Pubkey::new_unique();
let unrelated_stake_address = Pubkey::new_unique();
let mut unrelated_stake_account = stake_account.clone();
assert_ne!(unrelated_vote_address, vote_address);
unrelated_stake_account
.serialize_data(&StakeState::Stake(
Meta::default(),
new_stake(
1, /* stake */
&unrelated_vote_address,
&VoteState::default(),
1, /* activation_epoch */
&stake_config::Config::default(),
),
))
.unwrap();
process_instruction_deactivate_delinquent(
&unrelated_stake_address,
&unrelated_stake_account,
&vote_account,
&reference_vote_account,
Err(StakeError::VoteAddressMismatch.into()),
);
// `reference_vote_account` has consistently voted and `vote_account` voted once
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` ago.
// Instruction will succeed
let mut vote_state = VoteState::default();
vote_state
.increment_credits(current_epoch - MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch);
vote_account
.serialize_data(&VoteStateVersions::new_current(vote_state))
.unwrap();
process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Ok(()),
);
// `reference_vote_account` has consistently voted and `vote_account` voted once
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` - 1 epochs ago
// Instruction will fail
let mut vote_state = VoteState::default();
vote_state.increment_credits(
current_epoch - (MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION - 1) as Epoch,
);
vote_account
.serialize_data(&VoteStateVersions::new_current(vote_state))
.unwrap();
process_instruction_deactivate_delinquent(
&stake_address,
&stake_account,
&vote_account,
&reference_vote_account,
Err(StakeError::MinimumDelinquentEpochsForDeactivationNotMet.into()),
);
}
}

View File

@ -24,6 +24,7 @@ use {
config::Config,
instruction::{LockupArgs, StakeError},
program::id,
tools::{acceptable_reference_epoch_credits, eligible_for_deactivate_delinquent},
},
stake_history::{StakeHistory, StakeHistoryEntry},
transaction_context::{BorrowedAccount, InstructionContext, TransactionContext},
@ -128,7 +129,7 @@ fn redelegate(
Ok(())
}
fn new_stake(
pub(crate) fn new_stake(
stake: u64,
voter_pubkey: &Pubkey,
vote_state: &VoteState,
@ -862,6 +863,57 @@ pub fn withdraw(
Ok(())
}
pub(crate) fn deactivate_delinquent(
transaction_context: &TransactionContext,
instruction_context: &InstructionContext,
stake_account: &mut BorrowedAccount,
delinquent_vote_account_index: usize,
reference_vote_account_index: usize,
current_epoch: Epoch,
) -> Result<(), InstructionError> {
let delinquent_vote_account_pubkey = transaction_context.get_key_of_account_at_index(
instruction_context.get_index_in_transaction(delinquent_vote_account_index)?,
)?;
let delinquent_vote_account = instruction_context
.try_borrow_account(transaction_context, delinquent_vote_account_index)?;
if *delinquent_vote_account.get_owner() != solana_vote_program::id() {
return Err(InstructionError::IncorrectProgramId);
}
let delinquent_vote_state = delinquent_vote_account
.get_state::<VoteStateVersions>()?
.convert_to_current();
let reference_vote_account = instruction_context
.try_borrow_account(transaction_context, reference_vote_account_index)?;
if *reference_vote_account.get_owner() != solana_vote_program::id() {
return Err(InstructionError::IncorrectProgramId);
}
let reference_vote_state = reference_vote_account
.get_state::<VoteStateVersions>()?
.convert_to_current();
if !acceptable_reference_epoch_credits(&reference_vote_state.epoch_credits, current_epoch) {
return Err(StakeError::InsufficientReferenceVotes.into());
}
if let StakeState::Stake(meta, mut stake) = stake_account.get_state()? {
if stake.delegation.voter_pubkey != *delinquent_vote_account_pubkey {
return Err(StakeError::VoteAddressMismatch.into());
}
// Deactivate the stake account if its delegated vote account has never voted or has not
// voted in the last `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`
if eligible_for_deactivate_delinquent(&delinquent_vote_state.epoch_credits, current_epoch) {
stake.deactivate(current_epoch)?;
stake_account.set_state(&StakeState::Stake(meta, stake))
} else {
Err(StakeError::MinimumDelinquentEpochsForDeactivationNotMet.into())
}
} else {
Err(InstructionError::InvalidAccountData)
}
}
/// After calling `validate_delegated_amount()`, this struct contains calculated values that are used
/// by the caller.
struct ValidatedDelegatedInfo {

View File

@ -316,7 +316,7 @@ pub struct VoteState {
/// history of how many credits earned by the end of each epoch
/// each tuple is (Epoch, credits, prev_credits)
pub(crate) epoch_credits: Vec<(Epoch, u64, u64)>,
pub epoch_credits: Vec<(Epoch, u64, u64)>,
/// most recent timestamp submitted with a vote
pub last_timestamp: BlockTimestamp,
@ -1013,7 +1013,7 @@ impl VoteState {
self.votes.iter().map(|v| v.slot).collect()
}
fn current_epoch(&self) -> Epoch {
pub fn current_epoch(&self) -> Epoch {
if self.epoch_credits.is_empty() {
0
} else {

View File

@ -46,6 +46,17 @@ pub enum StakeError {
#[error("custodian signature not present")]
CustodianSignatureMissing,
#[error("insufficient voting activity in the reference vote account")]
InsufficientReferenceVotes,
#[error("stake account is not delegated to the provided vote account")]
VoteAddressMismatch,
#[error(
"stake account has not been delinquent for the minimum epochs required for deactivation"
)]
MinimumDelinquentEpochsForDeactivationNotMet,
}
impl<E> DecodeError<E> for StakeError {
@ -234,6 +245,19 @@ pub enum StakeInstruction {
///
/// [`get_minimum_delegation()`]: super::tools::get_minimum_delegation
GetMinimumDelegation,
/// Deactivate stake delegated to a vote account that has been delinquent for at least
/// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` epochs.
///
/// No signer is required for this instruction as it is a common good to deactivate abandoned
/// stake.
///
/// # Account references
/// 0. `[WRITE]` Delegated stake account
/// 1. `[]` Delinquent vote account for the delegated stake account
/// 2. `[]` Reference vote account that has voted at least once in the last
/// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` epochs
DeactivateDelinquent,
}
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
@ -698,6 +722,19 @@ pub fn get_minimum_delegation() -> Instruction {
)
}
pub fn deactivate_delinquent_stake(
stake_account: &Pubkey,
delinquent_vote_account: &Pubkey,
reference_vote_account: &Pubkey,
) -> Instruction {
let account_metas = vec![
AccountMeta::new(*stake_account, false),
AccountMeta::new_readonly(*delinquent_vote_account, false),
AccountMeta::new_readonly(*reference_vote_account, false),
];
Instruction::new_with_bincode(id(), &StakeInstruction::DeactivateDelinquent, account_metas)
}
#[cfg(test)]
mod tests {
use {super::*, crate::instruction::InstructionError};

View File

@ -10,3 +10,7 @@ pub mod program {
// NOTE: This constant will be deprecated soon; if possible, use
// `solana_stake_program::get_minimum_delegation()` instead.
pub const MINIMUM_STAKE_DELEGATION: u64 = 1;
/// The minimum number of epochs before stake account that is delegated to a delinquent vote
/// account may be unstaked with `StakeInstruction::DeactivateDelinquent`
pub const MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION: usize = 5;

View File

@ -1,5 +1,7 @@
//! Utility functions
use crate::program_error::ProgramError;
use crate::{
clock::Epoch, program_error::ProgramError, stake::MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION,
};
/// Helper function for programs to call [`GetMinimumDelegation`] and then fetch the return data
///
@ -36,3 +38,117 @@ fn get_minimum_delegation_return_data() -> Result<u64, ProgramError> {
})
.map(u64::from_le_bytes)
}
// Check if the provided `epoch_credits` demonstrate active voting over the previous
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`
pub fn acceptable_reference_epoch_credits(
epoch_credits: &[(Epoch, u64, u64)],
current_epoch: Epoch,
) -> bool {
if let Some(epoch_index) = epoch_credits
.len()
.checked_sub(MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION)
{
let mut epoch = current_epoch;
for (vote_epoch, ..) in epoch_credits[epoch_index..].iter().rev() {
if *vote_epoch != epoch {
return false;
}
epoch = epoch.saturating_sub(1);
}
true
} else {
false
}
}
// Check if the provided `epoch_credits` demonstrate delinquency over the previous
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`
pub fn eligible_for_deactivate_delinquent(
epoch_credits: &[(Epoch, u64, u64)],
current_epoch: Epoch,
) -> bool {
match epoch_credits.last() {
None => true,
Some((epoch, ..)) => {
if let Some(minimum_epoch) =
current_epoch.checked_sub(MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch)
{
*epoch <= minimum_epoch
} else {
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_acceptable_reference_epoch_credits() {
let epoch_credits = [];
assert!(!acceptable_reference_epoch_credits(&epoch_credits, 0));
let epoch_credits = [(0, 42, 42), (1, 42, 42), (2, 42, 42), (3, 42, 42)];
assert!(!acceptable_reference_epoch_credits(&epoch_credits, 3));
let epoch_credits = [
(0, 42, 42),
(1, 42, 42),
(2, 42, 42),
(3, 42, 42),
(4, 42, 42),
];
assert!(!acceptable_reference_epoch_credits(&epoch_credits, 3));
assert!(acceptable_reference_epoch_credits(&epoch_credits, 4));
let epoch_credits = [
(1, 42, 42),
(2, 42, 42),
(3, 42, 42),
(4, 42, 42),
(5, 42, 42),
];
assert!(acceptable_reference_epoch_credits(&epoch_credits, 5));
let epoch_credits = [
(0, 42, 42),
(2, 42, 42),
(3, 42, 42),
(4, 42, 42),
(5, 42, 42),
];
assert!(!acceptable_reference_epoch_credits(&epoch_credits, 5));
}
#[test]
fn test_eligible_for_deactivate_delinquent() {
let epoch_credits = [];
assert!(eligible_for_deactivate_delinquent(&epoch_credits, 42));
let epoch_credits = [(0, 42, 42)];
assert!(!eligible_for_deactivate_delinquent(&epoch_credits, 0));
let epoch_credits = [(0, 42, 42)];
assert!(!eligible_for_deactivate_delinquent(
&epoch_credits,
MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch - 1
));
assert!(eligible_for_deactivate_delinquent(
&epoch_credits,
MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch
));
let epoch_credits = [(100, 42, 42)];
assert!(!eligible_for_deactivate_delinquent(
&epoch_credits,
100 + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch - 1
));
assert!(eligible_for_deactivate_delinquent(
&epoch_credits,
100 + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch
));
}
}

View File

@ -271,6 +271,10 @@ pub mod update_syscall_base_costs {
solana_sdk::declare_id!("2h63t332mGCCsWK2nqqqHhN4U9ayyqhLVFvczznHDoTZ");
}
pub mod stake_deactivate_delinquent_instruction {
solana_sdk::declare_id!("437r62HoAdUb63amq3D7ENnBLDhHT2xY8eFkLJYVKK4x");
}
pub mod vote_withdraw_authority_may_change_authorized_voter {
solana_sdk::declare_id!("AVZS3ZsN4gi6Rkx2QUibYuSJG3S6QHib7xCYhG6vGJxU");
}
@ -401,6 +405,7 @@ lazy_static! {
(require_rent_exempt_accounts::id(), "require all new transaction accounts with data to be rent-exempt"),
(filter_votes_outside_slot_hashes::id(), "filter vote slots older than the slot hashes history"),
(update_syscall_base_costs::id(), "update syscall base costs"),
(stake_deactivate_delinquent_instruction::id(), "enable the deactivate delinquent stake instruction #23932"),
(vote_withdraw_authority_may_change_authorized_voter::id(), "vote account withdraw authority may change the authorized voter #22521"),
(spl_associated_token_account_v1_0_4::id(), "SPL Associated Token Account Program release version 1.0.4, tied to token 3.3.0 #22648"),
(reject_vote_account_close_unless_zero_credit_epoch::id(), "fail vote account withdraw to 0 unless account earned 0 credits in last completed epoch"),

View File

@ -273,6 +273,17 @@ pub fn parse_stake(
instruction_type: "getMinimumDelegation".to_string(),
info: Value::default(),
}),
StakeInstruction::DeactivateDelinquent => {
check_num_stake_accounts(&instruction.accounts, 3)?;
Ok(ParsedInstructionEnum {
instruction_type: "deactivateDeactive".to_string(),
info: json!({
"stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(),
"voteAccount": account_keys[instruction.accounts[1] as usize].to_string(),
"referenceVoteAccount": account_keys[instruction.accounts[3] as usize].to_string(),
}),
})
}
}
}