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

@ -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
));
}
}