Add StakeInstruction::DeactivateDelinquent
This commit is contained in:
parent
b9caa8cdfb
commit
57ff7371b4
@ -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,
|
||||
|
119
cli/src/stake.rs
119
cli/src/stake.rs
@ -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(
|
||||
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,
|
||||
&stake_authority.pubkey(),
|
||||
)]
|
||||
&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,
|
||||
|
@ -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,
|
||||
|
@ -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()),
|
||||
|
@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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};
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -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"),
|
||||
|
@ -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(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user