From 8c2d640686587aac9470a86d12ae7f8ab611ae1d Mon Sep 17 00:00:00 2001 From: HaoranYi Date: Tue, 30 May 2023 19:42:22 +0000 Subject: [PATCH] wip --- account-decoder/src/parse_stake.rs | 6 +- cli/src/cluster_query.rs | 2 +- cli/src/stake.rs | 3 +- cli/tests/stake.rs | 4 +- ledger-tool/src/main.rs | 2 +- programs/stake/src/stake_instruction.rs | 114 +++++++++++-------- programs/stake/src/stake_state.rs | 67 +++++++----- runtime/src/non_circulating_supply.rs | 2 +- runtime/src/stake_account.rs | 8 +- runtime/tests/stake.rs | 6 +- sdk/program/src/stake/instruction.rs | 3 + sdk/program/src/stake/state.rs | 140 +++++++++++++++++++++--- 12 files changed, 259 insertions(+), 98 deletions(-) diff --git a/account-decoder/src/parse_stake.rs b/account-decoder/src/parse_stake.rs index 4db60fecd62b94..f28443a05f4658 100644 --- a/account-decoder/src/parse_stake.rs +++ b/account-decoder/src/parse_stake.rs @@ -19,7 +19,7 @@ pub fn parse_stake(data: &[u8]) -> Result { meta: meta.into(), stake: None, }), - StakeState::Stake(meta, stake) => StakeAccountType::Delegated(UiStakeAccount { + StakeState::Stake(meta, stake, _) => StakeAccountType::Delegated(UiStakeAccount { meta: meta.into(), stake: Some(stake.into()), }), @@ -136,7 +136,7 @@ impl From for UiDelegation { #[cfg(test)] mod test { - use {super::*, bincode::serialize}; + use {super::*, bincode::serialize, solana_sdk::stake::state::DeactivationFlag}; #[test] fn test_parse_stake() { @@ -194,7 +194,7 @@ mod test { credits_observed: 10, }; - let stake_state = StakeState::Stake(meta, stake); + let stake_state = StakeState::Stake(meta, stake, DeactivationFlag::Empty); let stake_data = serialize(&stake_state).unwrap(); assert_eq!( parse_stake(&stake_data).unwrap(), diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index acef8ce112e5d4..77db0a5f801294 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1819,7 +1819,7 @@ pub fn process_show_stakes( }); } } - StakeState::Stake(_, stake) => { + StakeState::Stake(_, stake, _) => { if vote_account_pubkeys.is_none() || vote_account_pubkeys .unwrap() diff --git a/cli/src/stake.rs b/cli/src/stake.rs index c1e04a3af5c840..9e1726744b5f1a 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -1628,7 +1628,7 @@ pub fn process_deactivate_stake_account( let vote_account_address = match stake_account.state() { Ok(stake_state) => match stake_state { - StakeState::Stake(_, stake) => stake.delegation.voter_pubkey, + StakeState::Stake(_, stake, _) => stake.delegation.voter_pubkey, _ => { return Err(CliError::BadParameter(format!( "{stake_account_address} is not a delegated stake account", @@ -2195,6 +2195,7 @@ pub fn build_stake_state( lockup, }, stake, + _, ) => { let current_epoch = clock.epoch; let StakeActivationStatus { diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index b815ec09757d0b..6e14877272fa32 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -165,7 +165,7 @@ fn test_stake_redelegation() { let stake_state: StakeState = stake_account.state().unwrap(); let rent_exempt_reserve = match stake_state { - StakeState::Stake(meta, stake) => { + StakeState::Stake(meta, stake, _) => { assert_eq!(stake.delegation.voter_pubkey, vote_keypair.pubkey()); meta.rent_exempt_reserve } @@ -270,7 +270,7 @@ fn test_stake_redelegation() { let stake2_state: StakeState = stake2_account.state().unwrap(); match stake2_state { - StakeState::Stake(_meta, stake) => { + StakeState::Stake(_meta, stake, _) => { assert_eq!(stake.delegation.voter_pubkey, vote2_keypair.pubkey()); } _ => panic!("Unexpected stake2 state!"), diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index a0e901732d8089..6e7854bb334cfc 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -2861,7 +2861,7 @@ fn main() { .unwrap() .into_iter() { - if let Ok(StakeState::Stake(meta, stake)) = account.state() { + if let Ok(StakeState::Stake(meta, stake, _)) = account.state() { if vote_accounts_to_destake .contains(&stake.delegation.voter_pubkey) { diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 22a93730e24978..295c2fa02ea351 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -255,7 +255,7 @@ declare_process_instruction!(process_instruction, 750, |invoke_context| { let mut me = get_stake_account()?; let clock = get_sysvar_with_account_check::clock(invoke_context, instruction_context, 1)?; - deactivate(&mut me, &clock, &signers) + deactivate(invoke_context, &mut me, &clock, &signers) } Ok(StakeInstruction::SetLockup(lockup)) => { let mut me = get_stake_account()?; @@ -411,6 +411,7 @@ declare_process_instruction!(process_instruction, 750, |invoke_context| { let clock = invoke_context.get_sysvar_cache().get_clock()?; deactivate_delinquent( + invoke_context, transaction_context, instruction_context, &mut me, @@ -479,6 +480,7 @@ declare_process_instruction!(process_instruction, 750, |invoke_context| { #[cfg(test)] mod tests { + use { super::*, crate::stake_state::{ @@ -508,7 +510,9 @@ mod tests { set_lockup_checked, AuthorizeCheckedWithSeedArgs, AuthorizeWithSeedArgs, LockupArgs, StakeError, }, - state::{Authorized, Lockup, StakeActivationStatus, StakeAuthorize}, + state::{ + Authorized, DeactivationFlag, Lockup, StakeActivationStatus, StakeAuthorize, + }, MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION, }, stake_history::{StakeHistory, StakeHistoryEntry}, @@ -652,6 +656,7 @@ mod tests { }, ..Stake::default() }, + DeactivationFlag::Empty, ) } @@ -2763,7 +2768,7 @@ mod tests { StakeState::Initialized(_meta) => { assert_eq!(from(&accounts[0]).unwrap(), state); } - StakeState::Stake(_meta, _stake) => { + StakeState::Stake(_meta, _stake, _) => { let stake_0 = from(&accounts[0]).unwrap().stake(); assert_eq!(stake_0.unwrap().delegation.stake, stake_lamports / 2); } @@ -4358,8 +4363,8 @@ mod tests { // *must* equal the split amount. Otherwise, the split amount must first be used to // make the destination rent exempt, and then the leftover lamports are delegated. if expected_result.is_ok() { - assert_matches!(accounts[0].state().unwrap(), StakeState::Stake(_, _)); - if let StakeState::Stake(_, destination_stake) = accounts[1].state().unwrap() { + assert_matches!(accounts[0].state().unwrap(), StakeState::Stake(_, _, _)); + if let StakeState::Stake(_, destination_stake, _) = accounts[1].state().unwrap() { let destination_initial_rent_deficit = rent_exempt_reserve.saturating_sub(destination_starting_balance); let expected_destination_stake_delegation = @@ -4787,7 +4792,11 @@ mod tests { for split_to_state in &[ StakeState::Initialized(Meta::default()), - StakeState::Stake(Meta::default(), Stake::default()), + StakeState::Stake( + Meta::default(), + Stake::default(), + DeactivationFlag::default(), + ), StakeState::RewardsPool, ] { let split_to_account = AccountSharedData::new_data_with_space( @@ -4953,7 +4962,7 @@ mod tests { ); // verify no stake leakage in the case of a stake - if let StakeState::Stake(meta, stake) = state { + if let StakeState::Stake(meta, stake, deactivation_flag) = state { assert_eq!( accounts[1].state(), Ok(StakeState::Stake( @@ -4964,7 +4973,8 @@ mod tests { ..stake.delegation }, ..*stake - } + }, + *deactivation_flag, )) ); assert_eq!(accounts[0].lamports(), *minimum_balance,); @@ -5055,7 +5065,7 @@ mod tests { stake_lamports + initial_balance, ); - if let StakeState::Stake(meta, stake) = state { + if let StakeState::Stake(meta, stake, deactivation_flag) = state { let expected_stake = stake_lamports / 2 - (rent_exempt_reserve.saturating_sub(initial_balance)); assert_eq!( @@ -5068,7 +5078,8 @@ mod tests { ..stake.delegation }, ..stake - } + }, + deactivation_flag )), accounts[1].state(), ); @@ -5087,7 +5098,8 @@ mod tests { ..stake.delegation }, ..stake - } + }, + deactivation_flag, )), accounts[0].state(), ); @@ -5178,7 +5190,7 @@ mod tests { stake_lamports + initial_balance ); - if let StakeState::Stake(meta, stake) = state { + if let StakeState::Stake(meta, stake, deactivation_flag) = state { let expected_split_meta = Meta { authorized: Authorized::auto(&stake_address), rent_exempt_reserve: split_rent_exempt_reserve, @@ -5196,7 +5208,8 @@ mod tests { ..stake.delegation }, ..stake - } + }, + deactivation_flag, )), accounts[1].state() ); @@ -5215,7 +5228,8 @@ mod tests { ..stake.delegation }, ..stake - } + }, + deactivation_flag, )), accounts[0].state() ); @@ -5371,7 +5385,7 @@ mod tests { assert_eq!(Ok(*state), accounts[1].state()); assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); } - StakeState::Stake(meta, stake) => { + StakeState::Stake(meta, stake, deactivation_flag) => { assert_eq!( Ok(StakeState::Stake( *meta, @@ -5381,7 +5395,8 @@ mod tests { ..stake.delegation }, ..*stake - } + }, + *deactivation_flag )), accounts[1].state() ); @@ -5466,7 +5481,7 @@ mod tests { stake_lamports + initial_balance ); - if let StakeState::Stake(meta, stake) = state { + if let StakeState::Stake(meta, stake, deactivation_flag) = state { assert_eq!( Ok(StakeState::Stake( meta, @@ -5476,7 +5491,8 @@ mod tests { ..stake.delegation }, ..stake - } + }, + deactivation_flag, )), accounts[1].state() ); @@ -5588,7 +5604,7 @@ mod tests { ); assert_eq!(Ok(StakeState::Uninitialized), accounts[0].state()); } - StakeState::Stake(_meta, stake) => { + StakeState::Stake(_meta, stake, deactivation_flag) => { // Expected stake should reflect original stake amount so that extra lamports // from the rent_exempt_reserve inequality do not magically activate let expected_stake = stake_lamports - source_rent_exempt_reserve; @@ -5602,7 +5618,8 @@ mod tests { ..stake.delegation }, ..*stake - } + }, + *deactivation_flag, )), accounts[1].state() ); @@ -5717,7 +5734,7 @@ mod tests { StakeState::Initialized(meta) => { assert_eq!(accounts[0].state(), Ok(StakeState::Initialized(*meta)),); } - StakeState::Stake(meta, stake) => { + StakeState::Stake(meta, stake, deactivation_flag) => { let expected_stake = stake.delegation.stake + merge_from_state .stake() @@ -5736,7 +5753,8 @@ mod tests { ..stake.delegation }, ..*stake - } + }, + *deactivation_flag, )), ); } @@ -5770,7 +5788,7 @@ mod tests { }; let stake_account = AccountSharedData::new_data_with_space( stake_lamports, - &StakeState::Stake(meta, stake), + &StakeState::Stake(meta, stake, DeactivationFlag::Empty), StakeState::size_of(), &id(), ) @@ -6097,7 +6115,7 @@ mod tests { }; let stake_account = AccountSharedData::new_data_with_space( stake_lamports, - &StakeState::Stake(meta, stake), + &StakeState::Stake(meta, stake, DeactivationFlag::Empty), StakeState::size_of(), &id(), ) @@ -6113,7 +6131,7 @@ mod tests { }; let merge_from_account = AccountSharedData::new_data_with_space( merge_from_lamports, - &StakeState::Stake(meta, merge_from_stake), + &StakeState::Stake(meta, merge_from_stake, DeactivationFlag::Empty), StakeState::size_of(), &id(), ) @@ -6269,7 +6287,7 @@ mod tests { }; transaction_accounts[0] .1 - .set_state(&StakeState::Stake(meta, stake)) + .set_state(&StakeState::Stake(meta, stake, DeactivationFlag::Empty)) .unwrap(); } if clock.epoch == merge_from_deactivation_epoch { @@ -6283,7 +6301,11 @@ mod tests { }; transaction_accounts[1] .1 - .set_state(&StakeState::Stake(meta, merge_from_stake)) + .set_state(&StakeState::Stake( + meta, + merge_from_stake, + DeactivationFlag::Empty, + )) .unwrap(); } stake_history.add( @@ -6494,6 +6516,7 @@ mod tests { 1, /* activation_epoch */ &stake_config::Config::default(), ), + DeactivationFlag::Empty, ); let stake_account = AccountSharedData::new_data_with_space( @@ -6696,6 +6719,7 @@ mod tests { 1, /* activation_epoch */ &stake_config::Config::default(), ), + DeactivationFlag::Empty, )) .unwrap(); @@ -6789,6 +6813,7 @@ mod tests { activation_epoch, &stake_config::Config::default(), ), + DeactivationFlag::Empty, ); if let Some(expected_stake_activation_status) = expected_stake_activation_status { @@ -6919,7 +6944,7 @@ mod tests { ); assert_eq!(output_accounts[0].lamports(), rent_exempt_reserve); - if let StakeState::Stake(meta, stake) = + if let StakeState::Stake(meta, stake, _) = output_accounts[0].borrow().deserialize_data().unwrap() { assert_eq!(meta.rent_exempt_reserve, rent_exempt_reserve); @@ -6936,7 +6961,7 @@ mod tests { output_accounts[1].lamports(), minimum_delegation + rent_exempt_reserve ); - if let StakeState::Stake(meta, stake) = + if let StakeState::Stake(meta, stake, _) = output_accounts[1].borrow().deserialize_data().unwrap() { assert_eq!(meta.rent_exempt_reserve, rent_exempt_reserve); @@ -7081,7 +7106,7 @@ mod tests { output_accounts[1].lamports(), minimum_delegation + rent_exempt_reserve + 42 ); - if let StakeState::Stake(meta, stake) = + if let StakeState::Stake(meta, stake, _) = output_accounts[1].borrow().deserialize_data().unwrap() { assert_eq!(meta.rent_exempt_reserve, rent_exempt_reserve); @@ -7097,14 +7122,14 @@ mod tests { // let mut stake_account_over_allocated = prepare_stake_account(0 /*activation_epoch:*/, None); - if let StakeState::Stake(mut meta, stake) = stake_account_over_allocated + if let StakeState::Stake(mut meta, stake, deactivation_flag) = stake_account_over_allocated .borrow_mut() .deserialize_data() .unwrap() { meta.rent_exempt_reserve += 42; stake_account_over_allocated - .set_state(&StakeState::Stake(meta, stake)) + .set_state(&StakeState::Stake(meta, stake, deactivation_flag)) .unwrap(); } stake_account_over_allocated @@ -7127,7 +7152,7 @@ mod tests { ); assert_eq!(output_accounts[0].lamports(), rent_exempt_reserve + 42); - if let StakeState::Stake(meta, _stake) = + if let StakeState::Stake(meta, _stake, _) = output_accounts[0].borrow().deserialize_data().unwrap() { assert_eq!(meta.rent_exempt_reserve, rent_exempt_reserve + 42); @@ -7138,7 +7163,7 @@ mod tests { output_accounts[1].lamports(), minimum_delegation + rent_exempt_reserve, ); - if let StakeState::Stake(meta, stake) = + if let StakeState::Stake(meta, stake, _) = output_accounts[1].borrow().deserialize_data().unwrap() { assert_eq!(meta.rent_exempt_reserve, rent_exempt_reserve); @@ -7278,12 +7303,14 @@ mod tests { let mut deactivating_stake_account = prepare_stake_account(0 /*activation_epoch:*/, None); - if let StakeState::Stake(meta, mut stake) = deactivating_stake_account + if let StakeState::Stake(meta, mut stake, deactivate_flag) = deactivating_stake_account .borrow_mut() .deserialize_data() .unwrap() { - stake.deactivate(current_epoch).unwrap(); + stake + .deactivate(current_epoch, deactivate_flag, None) + .unwrap(); assert_eq!( StakeActivationStatus { effective: minimum_delegation + rent_exempt_reserve, @@ -7296,7 +7323,7 @@ mod tests { ); deactivating_stake_account - .set_state(&StakeState::Stake(meta, stake)) + .set_state(&StakeState::Stake(meta, stake, DeactivationFlag::Empty)) .unwrap(); } let _ = process_instruction_redelegate( @@ -7315,10 +7342,11 @@ mod tests { // (less than `minimum_delegation + rent_exempt_reserve`) // let mut stake_account_too_few_lamports = stake_account.clone(); - if let StakeState::Stake(meta, mut stake) = stake_account_too_few_lamports - .borrow_mut() - .deserialize_data() - .unwrap() + if let StakeState::Stake(meta, mut stake, deactivation_flag) = + stake_account_too_few_lamports + .borrow_mut() + .deserialize_data() + .unwrap() { stake.delegation.stake -= 1; assert_eq!( @@ -7326,7 +7354,7 @@ mod tests { minimum_delegation + rent_exempt_reserve - 1 ); stake_account_too_few_lamports - .set_state(&StakeState::Stake(meta, stake)) + .set_state(&StakeState::Stake(meta, stake, deactivation_flag)) .unwrap(); } else { panic!("Invalid stake_account"); @@ -7351,7 +7379,7 @@ mod tests { ); // - // Failure: redelegate to same vote addresss + // Failure: redelegate to same vote address // let _ = process_instruction_redelegate( &stake_address, diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 25abd0b5242410..92196e1787e2df 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -506,7 +506,7 @@ pub fn authorize( custodian: Option<&Pubkey>, ) -> Result<(), InstructionError> { match stake_account.get_state()? { - StakeState::Stake(mut meta, stake) => { + StakeState::Stake(mut meta, stake, deactivate_flag) => { meta.authorized.authorize( signers, new_authority, @@ -517,7 +517,7 @@ pub fn authorize( None }, )?; - stake_account.set_state(&StakeState::Stake(meta, stake)) + stake_account.set_state(&StakeState::Stake(meta, stake, deactivate_flag)) } StakeState::Initialized(mut meta) => { meta.authorized.authorize( @@ -609,9 +609,9 @@ pub fn delegate( clock.epoch, config, ); - stake_account.set_state(&StakeState::Stake(meta, stake)) + stake_account.set_state(&StakeState::Stake(meta, stake, DeactivationFlag::Empty)) } - StakeState::Stake(meta, mut stake) => { + StakeState::Stake(meta, mut stake, deactivation_flag) => { meta.authorized.check(signers, StakeAuthorize::Staker)?; let ValidatedDelegatedInfo { stake_amount } = validate_delegated_amount(&stake_account, &meta, feature_set)?; @@ -625,22 +625,24 @@ pub fn delegate( stake_history, config, )?; - stake_account.set_state(&StakeState::Stake(meta, stake)) + stake_account.set_state(&StakeState::Stake(meta, stake, deactivation_flag)) } _ => Err(InstructionError::InvalidAccountData), } } pub fn deactivate( + invoke_context: &InvokeContext, stake_account: &mut BorrowedAccount, clock: &Clock, signers: &HashSet, ) -> Result<(), InstructionError> { - if let StakeState::Stake(meta, mut stake) = stake_account.get_state()? { + if let StakeState::Stake(meta, mut stake, deactivate_flag) = stake_account.get_state()? { meta.authorized.check(signers, StakeAuthorize::Staker)?; - stake.deactivate(clock.epoch)?; - - stake_account.set_state(&StakeState::Stake(meta, stake)) + let stake_history = invoke_context.get_sysvar_cache().get_stake_history()?; + stake.deactivate(clock.epoch, deactivate_flag, Some(&stake_history))?; + // After deactivation, need to clear DeactivationFlag to Empty. + stake_account.set_state(&StakeState::Stake(meta, stake, DeactivationFlag::Empty)) } else { Err(InstructionError::InvalidAccountData) } @@ -657,9 +659,9 @@ pub fn set_lockup( meta.set_lockup(lockup, signers, clock)?; stake_account.set_state(&StakeState::Initialized(meta)) } - StakeState::Stake(mut meta, stake) => { + StakeState::Stake(mut meta, stake, deactivation_flag) => { meta.set_lockup(lockup, signers, clock)?; - stake_account.set_state(&StakeState::Stake(meta, stake)) + stake_account.set_state(&StakeState::Stake(meta, stake, deactivation_flag)) } _ => Err(InstructionError::InvalidAccountData), } @@ -696,7 +698,7 @@ pub fn split( drop(stake_account); match stake_state { - StakeState::Stake(meta, mut stake) => { + StakeState::Stake(meta, mut stake, deactivation_flag) => { meta.authorized.check(signers, StakeAuthorize::Staker)?; let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set); let validated_split_info = validate_split_amount( @@ -766,11 +768,15 @@ pub fn split( let mut stake_account = instruction_context .try_borrow_instruction_account(transaction_context, stake_account_index)?; - stake_account.set_state(&StakeState::Stake(meta, stake))?; + stake_account.set_state(&StakeState::Stake(meta, stake, deactivation_flag))?; drop(stake_account); let mut split = instruction_context .try_borrow_instruction_account(transaction_context, split_index)?; - split.set_state(&StakeState::Stake(split_meta, split_stake))?; + split.set_state(&StakeState::Stake( + split_meta, + split_stake, + deactivation_flag, + ))?; } StakeState::Initialized(meta) => { meta.authorized.check(signers, StakeAuthorize::Staker)?; @@ -954,7 +960,7 @@ pub fn redelegate( let vote_state = vote_account.get_state::()?; let (stake_meta, effective_stake) = - if let StakeState::Stake(meta, stake) = stake_account.get_state()? { + if let StakeState::Stake(meta, stake, _deactivation_flag) = stake_account.get_state()? { let stake_history = invoke_context.get_sysvar_cache().get_stake_history()?; let status = stake .delegation @@ -983,7 +989,7 @@ pub fn redelegate( // deactivate `stake_account` // // Note: This function also ensures `signers` contains the `StakeAuthorize::Staker` - deactivate(stake_account, &clock, signers)?; + deactivate(invoke_context, stake_account, &clock, signers)?; // transfer the effective stake to the uninitialized stake account stake_account.checked_sub_lamports(effective_stake)?; @@ -1010,6 +1016,7 @@ pub fn redelegate( clock.epoch, config, ), + DeactivationFlag::MustFullyActivateBeforeDeactivationIsPermitted, ))?; Ok(()) @@ -1041,7 +1048,7 @@ pub fn withdraw( let mut stake_account = instruction_context .try_borrow_instruction_account(transaction_context, stake_account_index)?; let (lockup, reserve, is_staked) = match stake_account.get_state()? { - StakeState::Stake(meta, stake) => { + StakeState::Stake(meta, stake, _deactivation_flag) => { meta.authorized .check(&signers, StakeAuthorize::Withdrawer)?; // if we have a deactivation epoch and we're in cooldown @@ -1130,6 +1137,7 @@ pub fn withdraw( } pub(crate) fn deactivate_delinquent( + invoke_context: &InvokeContext, transaction_context: &TransactionContext, instruction_context: &InstructionContext, stake_account: &mut BorrowedAccount, @@ -1163,7 +1171,7 @@ pub(crate) fn deactivate_delinquent( return Err(StakeError::InsufficientReferenceVotes.into()); } - if let StakeState::Stake(meta, mut stake) = stake_account.get_state()? { + if let StakeState::Stake(meta, mut stake, deactivate_flag) = stake_account.get_state()? { if stake.delegation.voter_pubkey != *delinquent_vote_account_pubkey { return Err(StakeError::VoteAddressMismatch.into()); } @@ -1171,8 +1179,10 @@ pub(crate) fn deactivate_delinquent( // 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)) + let stake_history = invoke_context.get_sysvar_cache().get_stake_history()?; + stake.deactivate(current_epoch, deactivate_flag, Some(&stake_history))?; + // After deactivation, need to clear DeactivationFlag to Empty. + stake_account.set_state(&StakeState::Stake(meta, stake, DeactivationFlag::Empty)) } else { Err(StakeError::MinimumDelinquentEpochsForDeactivationNotMet.into()) } @@ -1354,7 +1364,7 @@ impl MergeKind { stake_history: &StakeHistory, ) -> Result { match stake_state { - StakeState::Stake(meta, stake) => { + StakeState::Stake(meta, stake, _deactivation_flag) => { // stake must not be in a transient state. Transient here meaning // activating or deactivating with non-zero effective stake. let status = stake @@ -1470,7 +1480,7 @@ impl MergeKind { (Self::Inactive(_, _), Self::ActivationEpoch(_, _)) => None, (Self::ActivationEpoch(meta, mut stake), Self::Inactive(_, source_lamports)) => { stake.delegation.stake = checked_add(stake.delegation.stake, source_lamports)?; - Some(StakeState::Stake(meta, stake)) + Some(StakeState::Stake(meta, stake, DeactivationFlag::Empty)) } ( Self::ActivationEpoch(meta, mut stake), @@ -1486,7 +1496,7 @@ impl MergeKind { source_lamports, source_stake.credits_observed, )?; - Some(StakeState::Stake(meta, stake)) + Some(StakeState::Stake(meta, stake, DeactivationFlag::Empty)) } (Self::FullyActive(meta, mut stake), Self::FullyActive(_, source_stake)) => { // Don't stake the source account's `rent_exempt_reserve` to @@ -1499,7 +1509,7 @@ impl MergeKind { source_stake.delegation.stake, source_stake.credits_observed, )?; - Some(StakeState::Stake(meta, stake)) + Some(StakeState::Stake(meta, stake, DeactivationFlag::Empty)) } _ => return Err(StakeError::MergeMismatch.into()), }; @@ -1586,7 +1596,7 @@ pub fn redeem_rewards( inflation_point_calc_tracer: Option, credits_auto_rewind: bool, ) -> Result<(u64, u64), InstructionError> { - if let StakeState::Stake(meta, mut stake) = stake_state { + if let StakeState::Stake(meta, mut stake, deactivation_flag) = stake_state { if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { inflation_point_calc_tracer( &InflationPointCalculationEvent::EffectiveStakeAtRewardedEpoch( @@ -1611,7 +1621,7 @@ pub fn redeem_rewards( credits_auto_rewind, ) { stake_account.checked_add_lamports(stakers_reward)?; - stake_account.set_state(&StakeState::Stake(meta, stake))?; + stake_account.set_state(&StakeState::Stake(meta, stake, deactivation_flag))?; Ok((stakers_reward, voters_reward)) } else { @@ -1629,7 +1639,7 @@ pub fn calculate_points( vote_state: &VoteState, stake_history: Option<&StakeHistory>, ) -> Result { - if let StakeState::Stake(_meta, stake) = stake_state { + if let StakeState::Stake(_meta, stake, _deactivation_flag) = stake_state { Ok(calculate_stake_points( stake, vote_state, @@ -1791,6 +1801,7 @@ fn do_create_account( activation_epoch, &Config::default(), ), + DeactivationFlag::Empty, )) .expect("set_state"); @@ -3386,7 +3397,7 @@ mod tests { ..Stake::default() }; stake_account - .set_state(&StakeState::Stake(meta, stake)) + .set_state(&StakeState::Stake(meta, stake, DeactivationFlag::Empty)) .unwrap(); // activation_epoch succeeds assert_eq!( diff --git a/runtime/src/non_circulating_supply.rs b/runtime/src/non_circulating_supply.rs index 67e6a4ff1b267f..13ab3b21e31a28 100644 --- a/runtime/src/non_circulating_supply.rs +++ b/runtime/src/non_circulating_supply.rs @@ -60,7 +60,7 @@ pub fn calculate_non_circulating_supply(bank: &Arc) -> ScanResult { + StakeState::Stake(meta, _stake, _deactivation_flag) => { if meta.lockup.is_in_force(&clock, None) || withdraw_authority_list.contains(&meta.authorized.withdrawer) { diff --git a/runtime/src/stake_account.rs b/runtime/src/stake_account.rs index e52e67e3b9a138..928820eda0dea5 100644 --- a/runtime/src/stake_account.rs +++ b/runtime/src/stake_account.rs @@ -125,9 +125,13 @@ impl AbiExample for StakeAccount { fn example() -> Self { use solana_sdk::{ account::Account, - stake::state::{Meta, Stake}, + stake::state::{DeactivationFlag, Meta, Stake}, }; - let stake_state = StakeState::Stake(Meta::example(), Stake::example()); + let stake_state = StakeState::Stake( + Meta::example(), + Stake::example(), + DeactivationFlag::example(), + ); let mut account = Account::example(); account.data.resize(196, 0u8); account.owner = solana_stake_program::id(); diff --git a/runtime/tests/stake.rs b/runtime/tests/stake.rs index 6a74d87f6f2e13..378a6f76700900 100644 --- a/runtime/tests/stake.rs +++ b/runtime/tests/stake.rs @@ -350,7 +350,7 @@ fn test_stake_account_lifetime() { // Test that correct lamports are staked let account = bank.get_account(&stake_pubkey).expect("account not found"); let stake_state = account.state().expect("couldn't unpack account data"); - if let StakeState::Stake(_meta, stake) = stake_state { + if let StakeState::Stake(_meta, stake, _deactivation_flag) = stake_state { assert_eq!(stake.delegation.stake, stake_starting_delegation,); } else { panic!("wrong account type found") @@ -374,7 +374,7 @@ fn test_stake_account_lifetime() { // Test that lamports are still staked let account = bank.get_account(&stake_pubkey).expect("account not found"); let stake_state = account.state().expect("couldn't unpack account data"); - if let StakeState::Stake(_meta, stake) = stake_state { + if let StakeState::Stake(_meta, stake, _deactivation_flag) = stake_state { assert_eq!(stake.delegation.stake, stake_starting_delegation,); } else { panic!("wrong account type found") @@ -623,7 +623,7 @@ fn test_create_stake_account_from_seed() { // Test that correct lamports are staked let account = bank.get_account(&stake_pubkey).expect("account not found"); let stake_state = account.state().expect("couldn't unpack account data"); - if let StakeState::Stake(_meta, stake) = stake_state { + if let StakeState::Stake(_meta, stake, _) = stake_state { assert_eq!(stake.delegation.stake, delegation); } else { panic!("wrong account type found") diff --git a/sdk/program/src/stake/instruction.rs b/sdk/program/src/stake/instruction.rs index 67d784c38d4a43..54909cca81dcc0 100644 --- a/sdk/program/src/stake/instruction.rs +++ b/sdk/program/src/stake/instruction.rs @@ -66,6 +66,9 @@ pub enum StakeError { #[error("stake redelegation to the same vote account is not permitted")] RedelegateToSameVoteAccount, + + #[error("redelegated stake must be fully activated before deactivation")] + RedelegatedStakeMustFullyActivateBeforeDeactivationIsPermitted, } impl DecodeError for StakeError { diff --git a/sdk/program/src/stake/state.rs b/sdk/program/src/stake/state.rs index 9ebd6cfe51b374..f8e56870be8020 100644 --- a/sdk/program/src/stake/state.rs +++ b/sdk/program/src/stake/state.rs @@ -16,13 +16,34 @@ use { pub type StakeActivationStatus = StakeHistoryEntry; +#[repr(u8)] +#[derive( + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Clone, + Copy, + AbiExample, + BorshDeserialize, + BorshSchema, + BorshSerialize, +)] + +pub enum DeactivationFlag { + #[default] + Empty = 0, + MustFullyActivateBeforeDeactivationIsPermitted = 1, +} + #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)] #[allow(clippy::large_enum_variant)] pub enum StakeState { #[default] Uninitialized, Initialized(Meta), - Stake(Meta, Stake), + Stake(Meta, Stake, DeactivationFlag), RewardsPool, } @@ -38,7 +59,12 @@ impl BorshDeserialize for StakeState { 2 => { let meta: Meta = BorshDeserialize::deserialize(buf)?; let stake: Stake = BorshDeserialize::deserialize(buf)?; - Ok(StakeState::Stake(meta, stake)) + let deactivate_flag: DeactivationFlag = BorshDeserialize::deserialize(buf)?; + + // To make BorshSerializer compatible with bincode serializer, 3 padding bytes were added during serialization. + // So consume those 3 bytes here. + let _pad: [u8; 3] = BorshDeserialize::deserialize(buf)?; + Ok(StakeState::Stake(meta, stake, deactivate_flag)) } 3 => Ok(StakeState::RewardsPool), _ => Err(io::Error::new( @@ -57,10 +83,16 @@ impl BorshSerialize for StakeState { writer.write_all(&1u32.to_le_bytes())?; meta.serialize(writer) } - StakeState::Stake(meta, stake) => { + StakeState::Stake(meta, stake, deactivate_flag) => { writer.write_all(&2u32.to_le_bytes())?; meta.serialize(writer)?; - stake.serialize(writer) + stake.serialize(writer)?; + deactivate_flag.serialize(writer)?; + + // bincode serializer add 3 more padding bytes for `Stake` variant to pad the last + // u8 enum increase the size from 197 bytes to 200 bytes. + // To make BorshSerializer compatible with bincode serializer, add 3 padding bytes here. + writer.write_all(&[0; 3]) // padding } StakeState::RewardsPool => writer.write_all(&3u32.to_le_bytes()), } @@ -75,21 +107,21 @@ impl StakeState { pub fn stake(&self) -> Option { match self { - StakeState::Stake(_meta, stake) => Some(*stake), + StakeState::Stake(_meta, stake, _deactivation_flag) => Some(*stake), _ => None, } } pub fn delegation(&self) -> Option { match self { - StakeState::Stake(_meta, stake) => Some(stake.delegation), + StakeState::Stake(_meta, stake, _deactivation_flag) => Some(stake.delegation), _ => None, } } pub fn authorized(&self) -> Option { match self { - StakeState::Stake(meta, _stake) => Some(meta.authorized), + StakeState::Stake(meta, _stake, _deactivation_flag) => Some(meta.authorized), StakeState::Initialized(meta) => Some(meta.authorized), _ => None, } @@ -101,7 +133,7 @@ impl StakeState { pub fn meta(&self) -> Option { match self { - StakeState::Stake(meta, _stake) => Some(*meta), + StakeState::Stake(meta, _stake, _deactivation_flag) => Some(*meta), StakeState::Initialized(meta) => Some(*meta), _ => None, } @@ -564,13 +596,32 @@ impl Stake { Ok(new) } - pub fn deactivate(&mut self, epoch: Epoch) -> Result<(), StakeError> { + pub fn deactivate( + &mut self, + epoch: Epoch, + deactivation_flag: DeactivationFlag, + history: Option<&StakeHistory>, + ) -> Result<(), StakeError> { if self.delegation.deactivation_epoch != std::u64::MAX { - Err(StakeError::AlreadyDeactivated) - } else { - self.delegation.deactivation_epoch = epoch; - Ok(()) + return Err(StakeError::AlreadyDeactivated); + } + + // when deactivation_flag is set to + // MustFullyActivateBeforeDeactivationIsPermittedFlag, deactivation is + // only permitted when the stake delegation activating amount is zero. + if deactivation_flag == DeactivationFlag::MustFullyActivateBeforeDeactivationIsPermitted { + let status = self + .delegation + .stake_activating_and_deactivating(epoch, history); + if status.activating != 0 { + return Err( + StakeError::RedelegatedStakeMustFullyActivateBeforeDeactivationIsPermitted, + ); + } } + + self.delegation.deactivation_epoch = epoch; + Ok(()) } } @@ -629,6 +680,7 @@ mod test { }, credits_observed: 1, }, + DeactivationFlag::Empty, )); } @@ -663,6 +715,7 @@ mod test { }, credits_observed: 1, }, + DeactivationFlag::MustFullyActivateBeforeDeactivationIsPermitted, )); } @@ -690,4 +743,65 @@ mod test { }) ); } + + #[test] + fn stake_flag_member_offset() { + const FLAG_OFFSET: usize = 196; + + let stake = StakeState::Stake( + Meta { + rent_exempt_reserve: 1, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + }, + Stake { + delegation: Delegation { + voter_pubkey: Pubkey::new_unique(), + stake: u64::MAX, + activation_epoch: Epoch::MAX, + deactivation_epoch: Epoch::MAX, + warmup_cooldown_rate: f64::MAX, + }, + credits_observed: 1, + }, + DeactivationFlag::MustFullyActivateBeforeDeactivationIsPermitted, + ); + + let bincode_serialized = serialize(&stake).unwrap(); + let borsh_serialized = StakeState::try_to_vec(&stake).unwrap(); + + assert_eq!(bincode_serialized[FLAG_OFFSET], 1); + assert_eq!(borsh_serialized[FLAG_OFFSET], 1); + + let stake = StakeState::Stake( + Meta { + rent_exempt_reserve: 1, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + }, + Stake { + delegation: Delegation { + voter_pubkey: Pubkey::new_unique(), + stake: u64::MAX, + activation_epoch: Epoch::MAX, + deactivation_epoch: Epoch::MAX, + warmup_cooldown_rate: f64::MAX, + }, + credits_observed: 1, + }, + DeactivationFlag::Empty, + ); + + let bincode_serialized = serialize(&stake).unwrap(); + let borsh_serialized = StakeState::try_to_vec(&stake).unwrap(); + + assert_eq!(bincode_serialized[FLAG_OFFSET], 0); + assert_eq!(borsh_serialized[FLAG_OFFSET], 0); + } }