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

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