From 2b50433099b95f3d0a67ad2365e0edefe23cfb9c Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Mon, 25 Jan 2021 12:32:33 -0800 Subject: [PATCH] Remove legacy_stake program --- programs/stake/src/legacy_stake_processor.rs | 88 - programs/stake/src/legacy_stake_state.rs | 5459 ------------------ programs/stake/src/lib.rs | 2 - runtime/src/builtins.rs | 33 +- 4 files changed, 9 insertions(+), 5573 deletions(-) delete mode 100644 programs/stake/src/legacy_stake_processor.rs delete mode 100644 programs/stake/src/legacy_stake_state.rs diff --git a/programs/stake/src/legacy_stake_processor.rs b/programs/stake/src/legacy_stake_processor.rs deleted file mode 100644 index 014898d19b..0000000000 --- a/programs/stake/src/legacy_stake_processor.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::{config, legacy_stake_state::StakeAccount, stake_instruction::StakeInstruction}; -use log::*; -use solana_sdk::{ - instruction::InstructionError, - keyed_account::{from_keyed_account, get_signers, next_keyed_account, KeyedAccount}, - process_instruction::InvokeContext, - program_utils::limited_deserialize, - pubkey::Pubkey, - sysvar::{clock::Clock, rent::Rent, stake_history::StakeHistory}, -}; - -pub fn process_instruction( - _program_id: &Pubkey, - keyed_accounts: &[KeyedAccount], - data: &[u8], - _invoke_context: &mut dyn InvokeContext, -) -> Result<(), InstructionError> { - trace!("process_instruction: {:?}", data); - trace!("keyed_accounts: {:?}", keyed_accounts); - - let signers = get_signers(keyed_accounts); - - let keyed_accounts = &mut keyed_accounts.iter(); - let me = &next_keyed_account(keyed_accounts)?; - - match limited_deserialize(data)? { - StakeInstruction::Initialize(authorized, lockup) => me.initialize( - &authorized, - &lockup, - &from_keyed_account::(next_keyed_account(keyed_accounts)?)?, - ), - StakeInstruction::Authorize(authorized_pubkey, stake_authorize) => { - me.authorize(&signers, &authorized_pubkey, stake_authorize) - } - StakeInstruction::AuthorizeWithSeed(args) => { - let authority_base = next_keyed_account(keyed_accounts)?; - me.authorize_with_seed( - &authority_base, - &args.authority_seed, - &args.authority_owner, - &args.new_authorized_pubkey, - args.stake_authorize, - ) - } - StakeInstruction::DelegateStake => { - let vote = next_keyed_account(keyed_accounts)?; - - me.delegate( - &vote, - &from_keyed_account::(next_keyed_account(keyed_accounts)?)?, - &from_keyed_account::(next_keyed_account(keyed_accounts)?)?, - &config::from_keyed_account(next_keyed_account(keyed_accounts)?)?, - &signers, - ) - } - StakeInstruction::Split(lamports) => { - let split_stake = &next_keyed_account(keyed_accounts)?; - me.split(lamports, split_stake, &signers) - } - StakeInstruction::Merge => { - let source_stake = &next_keyed_account(keyed_accounts)?; - me.merge( - source_stake, - &from_keyed_account::(next_keyed_account(keyed_accounts)?)?, - &from_keyed_account::(next_keyed_account(keyed_accounts)?)?, - &signers, - ) - } - - StakeInstruction::Withdraw(lamports) => { - let to = &next_keyed_account(keyed_accounts)?; - me.withdraw( - lamports, - to, - &from_keyed_account::(next_keyed_account(keyed_accounts)?)?, - &from_keyed_account::(next_keyed_account(keyed_accounts)?)?, - next_keyed_account(keyed_accounts)?, - keyed_accounts.next(), - ) - } - StakeInstruction::Deactivate => me.deactivate( - &from_keyed_account::(next_keyed_account(keyed_accounts)?)?, - &signers, - ), - - StakeInstruction::SetLockup(lockup) => me.set_lockup(&lockup, &signers), - } -} diff --git a/programs/stake/src/legacy_stake_state.rs b/programs/stake/src/legacy_stake_state.rs deleted file mode 100644 index c8fe272396..0000000000 --- a/programs/stake/src/legacy_stake_state.rs +++ /dev/null @@ -1,5459 +0,0 @@ -//! Legacy stake state -//! The minimum code needed to support the legacy_stake_processor - -use crate::{ - config::Config, - id, - stake_instruction::{LockupArgs, StakeError}, - stake_state::{ - Authorized, Delegation, InflationPointCalculationEvent, Lockup, Meta, PointValue, - StakeAuthorize, - }, -}; -use serde_derive::{Deserialize, Serialize}; -use solana_sdk::{ - account::Account, - account_utils::{State, StateMut}, - clock::{Clock, Epoch}, - instruction::InstructionError, - keyed_account::KeyedAccount, - pubkey::Pubkey, - rent::{Rent, ACCOUNT_STORAGE_OVERHEAD}, - stake_history::StakeHistory, -}; -use solana_vote_program::vote_state::{VoteState, VoteStateVersions}; -use std::{collections::HashSet, convert::TryFrom}; - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)] -#[allow(clippy::large_enum_variant)] -pub enum StakeState { - Uninitialized, - Initialized(Meta), - Stake(Meta, Stake), - RewardsPool, -} - -impl Default for StakeState { - fn default() -> Self { - StakeState::Uninitialized - } -} - -impl StakeState { - pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 { - rent.minimum_balance(std::mem::size_of::()) - } - - // utility function, used by Stakes, tests - pub fn from(account: &Account) -> Option { - account.state().ok() - } - - pub fn stake_from(account: &Account) -> Option { - Self::from(account).and_then(|state: Self| state.stake()) - } - pub fn stake(&self) -> Option { - match self { - StakeState::Stake(_meta, stake) => Some(*stake), - _ => None, - } - } - - pub fn delegation_from(account: &Account) -> Option { - Self::from(account).and_then(|state: Self| state.delegation()) - } - pub fn delegation(&self) -> Option { - match self { - StakeState::Stake(_meta, stake) => Some(stake.delegation), - _ => None, - } - } - - pub fn authorized_from(account: &Account) -> Option { - Self::from(account).and_then(|state: Self| state.authorized()) - } - - pub fn authorized(&self) -> Option { - match self { - StakeState::Stake(meta, _stake) => Some(meta.authorized), - StakeState::Initialized(meta) => Some(meta.authorized), - _ => None, - } - } - - pub fn lockup_from(account: &Account) -> Option { - Self::from(account).and_then(|state: Self| state.lockup()) - } - - pub fn lockup(&self) -> Option { - match self { - StakeState::Stake(meta, _stake) => Some(meta.lockup), - StakeState::Initialized(meta) => Some(meta.lockup), - _ => None, - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)] -pub struct Stake { - pub delegation: Delegation, - /// credits observed is credits from vote account state when delegated or redeemed - pub credits_observed: u64, -} - -impl Stake { - pub fn stake( - &self, - epoch: Epoch, - history: Option<&StakeHistory>, - fix_stake_deactivate: bool, - ) -> u64 { - self.delegation.stake(epoch, history, fix_stake_deactivate) - } - - pub fn redeem_rewards( - &mut self, - point_value: &PointValue, - vote_state: &VoteState, - stake_history: Option<&StakeHistory>, - inflation_point_calc_tracer: &mut Option, - fix_stake_deactivate: bool, - ) -> Option<(u64, u64)> { - self.calculate_rewards( - point_value, - vote_state, - stake_history, - inflation_point_calc_tracer, - fix_stake_deactivate, - ) - .map(|(stakers_reward, voters_reward, credits_observed)| { - if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer { - inflation_point_calc_tracer(&InflationPointCalculationEvent::CreditsObserved( - self.credits_observed, - credits_observed, - )); - } - self.credits_observed = credits_observed; - self.delegation.stake += stakers_reward; - (stakers_reward, voters_reward) - }) - } - - pub fn calculate_points( - &self, - vote_state: &VoteState, - stake_history: Option<&StakeHistory>, - inflation_point_calc_tracer: &mut Option, - fix_stake_deactivate: bool, - ) -> u128 { - self.calculate_points_and_credits( - vote_state, - stake_history, - inflation_point_calc_tracer, - fix_stake_deactivate, - ) - .0 - } - - /// for a given stake and vote_state, calculate how many - /// points were earned (credits * stake) and new value - /// for credits_observed were the points paid - fn calculate_points_and_credits( - &self, - new_vote_state: &VoteState, - stake_history: Option<&StakeHistory>, - inflation_point_calc_tracer: &mut Option, - fix_stake_deactivate: bool, - ) -> (u128, u64) { - // if there is no newer credits since observed, return no point - if new_vote_state.credits() <= self.credits_observed { - if fix_stake_deactivate { - return (0, self.credits_observed); - } else { - return (0, 0); - } - } - - let mut points = 0; - let mut new_credits_observed = self.credits_observed; - - for (epoch, final_epoch_credits, initial_epoch_credits) in - new_vote_state.epoch_credits().iter().copied() - { - let stake = u128::from(self.delegation.stake( - epoch, - stake_history, - fix_stake_deactivate, - )); - - // figure out how much this stake has seen that - // for which the vote account has a record - let earned_credits = if self.credits_observed < initial_epoch_credits { - // the staker observed the entire epoch - final_epoch_credits - initial_epoch_credits - } else if self.credits_observed < final_epoch_credits { - // the staker registered sometime during the epoch, partial credit - final_epoch_credits - new_credits_observed - } else { - // the staker has already observed or been redeemed this epoch - // or was activated after this epoch - 0 - }; - let earned_credits = u128::from(earned_credits); - - // don't want to assume anything about order of the iterator... - new_credits_observed = new_credits_observed.max(final_epoch_credits); - - // finally calculate points for this epoch - let earned_points = stake * earned_credits; - points += earned_points; - - if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer { - inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints( - epoch, - stake, - earned_credits, - earned_points, - )); - } - } - - (points, new_credits_observed) - } - - /// for a given stake and vote_state, calculate what distributions and what updates should be made - /// returns a tuple in the case of a payout of: - /// * staker_rewards to be distributed - /// * voter_rewards to be distributed - /// * new value for credits_observed in the stake - // returns None if there's no payout or if any deserved payout is < 1 lamport - pub fn calculate_rewards( - &self, - point_value: &PointValue, - vote_state: &VoteState, - stake_history: Option<&StakeHistory>, - inflation_point_calc_tracer: &mut Option, - fix_stake_deactivate: bool, - ) -> Option<(u64, u64, u64)> { - let (points, credits_observed) = self.calculate_points_and_credits( - vote_state, - stake_history, - inflation_point_calc_tracer, - fix_stake_deactivate, - ); - - // Drive credits_observed forward unconditionally when rewards are disabled - if point_value.rewards == 0 && fix_stake_deactivate { - return Some((0, 0, credits_observed)); - } - - if points == 0 || point_value.points == 0 { - return None; - } - - let rewards = points - .checked_mul(u128::from(point_value.rewards)) - .unwrap() - .checked_div(point_value.points) - .unwrap(); - - let rewards = u64::try_from(rewards).unwrap(); - - // don't bother trying to split if fractional lamports got truncated - if rewards == 0 { - return None; - } - let (voter_rewards, staker_rewards, is_split) = vote_state.commission_split(rewards); - if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer { - inflation_point_calc_tracer(&InflationPointCalculationEvent::SplitRewards( - rewards, - voter_rewards, - staker_rewards, - (*point_value).clone(), - )); - } - - if (voter_rewards == 0 || staker_rewards == 0) && is_split { - // don't collect if we lose a whole lamport somewhere - // is_split means there should be tokens on both sides, - // uncool to move credits_observed if one side didn't get paid - return None; - } - - Some((staker_rewards, voter_rewards, credits_observed)) - } - - fn redelegate( - &mut self, - stake_lamports: u64, - voter_pubkey: &Pubkey, - vote_state: &VoteState, - clock: &Clock, - stake_history: &StakeHistory, - config: &Config, - ) -> Result<(), StakeError> { - // can't redelegate if stake is active. either the stake - // is freshly activated or has fully de-activated. redelegation - // implies re-activation - if self.stake(clock.epoch, Some(stake_history), true) != 0 { - return Err(StakeError::TooSoonToRedelegate); - } - self.delegation.stake = stake_lamports; - self.delegation.activation_epoch = clock.epoch; - self.delegation.deactivation_epoch = std::u64::MAX; - self.delegation.voter_pubkey = *voter_pubkey; - self.delegation.warmup_cooldown_rate = config.warmup_cooldown_rate; - self.credits_observed = vote_state.credits(); - Ok(()) - } - - fn split( - &mut self, - remaining_stake_delta: u64, - split_stake_amount: u64, - ) -> Result { - if remaining_stake_delta > self.delegation.stake { - return Err(StakeError::InsufficientStake); - } - self.delegation.stake -= remaining_stake_delta; - let new = Self { - delegation: Delegation { - stake: split_stake_amount, - ..self.delegation - }, - ..*self - }; - Ok(new) - } - - fn new( - stake: u64, - voter_pubkey: &Pubkey, - vote_state: &VoteState, - activation_epoch: Epoch, - config: &Config, - ) -> Self { - Self { - delegation: Delegation::new( - voter_pubkey, - stake, - activation_epoch, - config.warmup_cooldown_rate, - ), - credits_observed: vote_state.credits(), - } - } - - fn deactivate(&mut self, epoch: Epoch) -> Result<(), StakeError> { - if self.delegation.deactivation_epoch != std::u64::MAX { - Err(StakeError::AlreadyDeactivated) - } else { - self.delegation.deactivation_epoch = epoch; - Ok(()) - } - } -} - -pub trait StakeAccount { - fn initialize( - &self, - authorized: &Authorized, - lockup: &Lockup, - rent: &Rent, - ) -> Result<(), InstructionError>; - fn authorize( - &self, - signers: &HashSet, - new_authority: &Pubkey, - stake_authorize: StakeAuthorize, - ) -> Result<(), InstructionError>; - fn authorize_with_seed( - &self, - authority_base: &KeyedAccount, - authority_seed: &str, - authority_owner: &Pubkey, - new_authority: &Pubkey, - stake_authorize: StakeAuthorize, - ) -> Result<(), InstructionError>; - fn delegate( - &self, - vote_account: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - config: &Config, - signers: &HashSet, - ) -> Result<(), InstructionError>; - fn deactivate(&self, clock: &Clock, signers: &HashSet) -> Result<(), InstructionError>; - fn set_lockup( - &self, - lockup: &LockupArgs, - signers: &HashSet, - ) -> Result<(), InstructionError>; - fn split( - &self, - lamports: u64, - split_stake: &KeyedAccount, - signers: &HashSet, - ) -> Result<(), InstructionError>; - fn merge( - &self, - source_stake: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - signers: &HashSet, - ) -> Result<(), InstructionError>; - fn withdraw( - &self, - lamports: u64, - to: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - withdraw_authority: &KeyedAccount, - custodian: Option<&KeyedAccount>, - ) -> Result<(), InstructionError>; -} - -impl<'a> StakeAccount for KeyedAccount<'a> { - fn initialize( - &self, - authorized: &Authorized, - lockup: &Lockup, - rent: &Rent, - ) -> Result<(), InstructionError> { - if self.data_len()? != std::mem::size_of::() { - return Err(InstructionError::InvalidAccountData); - } - if let StakeState::Uninitialized = self.state()? { - let rent_exempt_reserve = rent.minimum_balance(self.data_len()?); - - if rent_exempt_reserve < self.lamports()? { - self.set_state(&StakeState::Initialized(Meta { - rent_exempt_reserve, - authorized: *authorized, - lockup: *lockup, - })) - } else { - Err(InstructionError::InsufficientFunds) - } - } else { - Err(InstructionError::InvalidAccountData) - } - } - - /// Authorize the given pubkey to manage stake (deactivate, withdraw). This may be called - /// multiple times, but will implicitly withdraw authorization from the previously authorized - /// staker. The default staker is the owner of the stake account's pubkey. - fn authorize( - &self, - signers: &HashSet, - new_authority: &Pubkey, - stake_authorize: StakeAuthorize, - ) -> Result<(), InstructionError> { - match self.state()? { - StakeState::Stake(mut meta, stake) => { - meta.authorized - .authorize(signers, new_authority, stake_authorize)?; - self.set_state(&StakeState::Stake(meta, stake)) - } - StakeState::Initialized(mut meta) => { - meta.authorized - .authorize(signers, new_authority, stake_authorize)?; - self.set_state(&StakeState::Initialized(meta)) - } - _ => Err(InstructionError::InvalidAccountData), - } - } - fn authorize_with_seed( - &self, - authority_base: &KeyedAccount, - authority_seed: &str, - authority_owner: &Pubkey, - new_authority: &Pubkey, - stake_authorize: StakeAuthorize, - ) -> Result<(), InstructionError> { - let mut signers = HashSet::default(); - if let Some(base_pubkey) = authority_base.signer_key() { - signers.insert(Pubkey::create_with_seed( - base_pubkey, - authority_seed, - authority_owner, - )?); - } - self.authorize(&signers, &new_authority, stake_authorize) - } - fn delegate( - &self, - vote_account: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - config: &Config, - signers: &HashSet, - ) -> Result<(), InstructionError> { - if vote_account.owner()? != solana_vote_program::id() { - return Err(InstructionError::IncorrectProgramId); - } - - match self.state()? { - StakeState::Initialized(meta) => { - meta.authorized.check(signers, StakeAuthorize::Staker)?; - let stake = Stake::new( - self.lamports()?.saturating_sub(meta.rent_exempt_reserve), // can't stake the rent ;) - vote_account.unsigned_key(), - &State::::state(vote_account)?.convert_to_current(), - clock.epoch, - config, - ); - self.set_state(&StakeState::Stake(meta, stake)) - } - StakeState::Stake(meta, mut stake) => { - meta.authorized.check(signers, StakeAuthorize::Staker)?; - stake.redelegate( - self.lamports()?.saturating_sub(meta.rent_exempt_reserve), // can't stake the rent ;) - vote_account.unsigned_key(), - &State::::state(vote_account)?.convert_to_current(), - clock, - stake_history, - config, - )?; - self.set_state(&StakeState::Stake(meta, stake)) - } - _ => Err(InstructionError::InvalidAccountData), - } - } - fn deactivate(&self, clock: &Clock, signers: &HashSet) -> Result<(), InstructionError> { - if let StakeState::Stake(meta, mut stake) = self.state()? { - meta.authorized.check(signers, StakeAuthorize::Staker)?; - stake.deactivate(clock.epoch)?; - - self.set_state(&StakeState::Stake(meta, stake)) - } else { - Err(InstructionError::InvalidAccountData) - } - } - fn set_lockup( - &self, - lockup: &LockupArgs, - signers: &HashSet, - ) -> Result<(), InstructionError> { - match self.state()? { - StakeState::Initialized(mut meta) => { - meta.set_lockup(lockup, signers)?; - self.set_state(&StakeState::Initialized(meta)) - } - StakeState::Stake(mut meta, stake) => { - meta.set_lockup(lockup, signers)?; - self.set_state(&StakeState::Stake(meta, stake)) - } - _ => Err(InstructionError::InvalidAccountData), - } - } - - fn split( - &self, - lamports: u64, - split: &KeyedAccount, - signers: &HashSet, - ) -> Result<(), InstructionError> { - if split.owner()? != id() { - return Err(InstructionError::IncorrectProgramId); - } - - if let StakeState::Uninitialized = split.state()? { - // verify enough account lamports - if lamports > self.lamports()? { - return Err(InstructionError::InsufficientFunds); - } - - match self.state()? { - StakeState::Stake(meta, mut stake) => { - meta.authorized.check(signers, StakeAuthorize::Staker)?; - let split_rent_exempt_reserve = calculate_split_rent_exempt_reserve( - meta.rent_exempt_reserve, - self.data_len()? as u64, - split.data_len()? as u64, - ); - - // verify enough lamports for rent in new split account - if lamports < split_rent_exempt_reserve.saturating_sub(split.lamports()?) - // verify full withdrawal can cover rent in new split account - || (lamports < split_rent_exempt_reserve && lamports == self.lamports()?) - // if not full withdrawal - || (lamports != self.lamports()? - // verify more than 0 stake left in previous stake - && (lamports + meta.rent_exempt_reserve >= self.lamports()? - // and verify more than 0 stake in new split account - || lamports - <= split_rent_exempt_reserve.saturating_sub(split.lamports()?))) - { - return Err(InstructionError::InsufficientFunds); - } - // split the stake, subtract rent_exempt_balance unless - // the destination account already has those lamports - // in place. - // this means that the new stake account will have a stake equivalent to - // lamports minus rent_exempt_reserve if it starts out with a zero balance - let (remaining_stake_delta, split_stake_amount) = if lamports - == self.lamports()? - { - // If split amount equals the full source stake, the new split stake must - // equal the same amount, regardless of any current lamport balance in the - // split account. Since split accounts retain the state of their source - // account, this prevents any magic activation of stake by prefunding the - // split account. - // The new split stake also needs to ignore any positive delta between the - // original rent_exempt_reserve and the split_rent_exempt_reserve, in order - // to prevent magic activation of stake by splitting between accounts of - // different sizes. - let remaining_stake_delta = - lamports.saturating_sub(meta.rent_exempt_reserve); - let split_stake_amount = std::cmp::min( - lamports - split_rent_exempt_reserve, - remaining_stake_delta, - ); - (remaining_stake_delta, split_stake_amount) - } else { - // Otherwise, the new split stake should reflect the entire split - // requested, less any lamports needed to cover the split_rent_exempt_reserve - ( - lamports, - lamports - split_rent_exempt_reserve.saturating_sub(split.lamports()?), - ) - }; - let split_stake = stake.split(remaining_stake_delta, split_stake_amount)?; - let mut split_meta = meta; - split_meta.rent_exempt_reserve = split_rent_exempt_reserve; - - self.set_state(&StakeState::Stake(meta, stake))?; - split.set_state(&StakeState::Stake(split_meta, split_stake))?; - } - StakeState::Initialized(meta) => { - meta.authorized.check(signers, StakeAuthorize::Staker)?; - let split_rent_exempt_reserve = calculate_split_rent_exempt_reserve( - meta.rent_exempt_reserve, - self.data_len()? as u64, - split.data_len()? as u64, - ); - - // enough lamports for rent in new stake - if lamports < split_rent_exempt_reserve - // if not full withdrawal - || (lamports != self.lamports()? - // verify more than 0 stake left in previous stake - && (lamports + meta.rent_exempt_reserve >= self.lamports()? - // and verify more than 0 stake in new split account - || lamports - <= split_rent_exempt_reserve.saturating_sub(split.lamports()?))) - { - return Err(InstructionError::InsufficientFunds); - } - - let mut split_meta = meta; - split_meta.rent_exempt_reserve = split_rent_exempt_reserve; - split.set_state(&StakeState::Initialized(split_meta))?; - } - StakeState::Uninitialized => { - if !signers.contains(&self.unsigned_key()) { - return Err(InstructionError::MissingRequiredSignature); - } - } - _ => return Err(InstructionError::InvalidAccountData), - } - - split.try_account_ref_mut()?.lamports += lamports; - self.try_account_ref_mut()?.lamports -= lamports; - Ok(()) - } else { - Err(InstructionError::InvalidAccountData) - } - } - - fn merge( - &self, - source_account: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - signers: &HashSet, - ) -> Result<(), InstructionError> { - // Ensure source isn't spoofed - if source_account.owner()? != id() { - return Err(InstructionError::IncorrectProgramId); - } - // Close the self-reference loophole - if source_account.unsigned_key() == self.unsigned_key() { - return Err(InstructionError::InvalidArgument); - } - - let stake_merge_kind = MergeKind::get_if_mergeable(self, clock, stake_history)?; - let meta = stake_merge_kind.meta(); - - // Authorized staker is allowed to split/merge accounts - meta.authorized.check(signers, StakeAuthorize::Staker)?; - - let source_merge_kind = MergeKind::get_if_mergeable(source_account, clock, stake_history)?; - - if let Some(merged_state) = stake_merge_kind.merge(source_merge_kind)? { - self.set_state(&merged_state)?; - } - - // Drain the source stake account - let lamports = source_account.lamports()?; - source_account.try_account_ref_mut()?.lamports -= lamports; - self.try_account_ref_mut()?.lamports += lamports; - Ok(()) - } - - fn withdraw( - &self, - lamports: u64, - to: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - withdraw_authority: &KeyedAccount, - custodian: Option<&KeyedAccount>, - ) -> Result<(), InstructionError> { - let mut signers = HashSet::new(); - let withdraw_authority_pubkey = withdraw_authority - .signer_key() - .ok_or(InstructionError::MissingRequiredSignature)?; - signers.insert(*withdraw_authority_pubkey); - - let (lockup, reserve, is_staked) = match self.state()? { - StakeState::Stake(meta, stake) => { - meta.authorized - .check(&signers, StakeAuthorize::Withdrawer)?; - // if we have a deactivation epoch and we're in cooldown - let staked = if clock.epoch >= stake.delegation.deactivation_epoch { - stake - .delegation - .stake(clock.epoch, Some(stake_history), true) - } else { - // Assume full stake if the stake account hasn't been - // de-activated, because in the future the exposed stake - // might be higher than stake.stake() due to warmup - stake.delegation.stake - }; - - (meta.lockup, staked + meta.rent_exempt_reserve, staked != 0) - } - StakeState::Initialized(meta) => { - meta.authorized - .check(&signers, StakeAuthorize::Withdrawer)?; - - (meta.lockup, meta.rent_exempt_reserve, false) - } - StakeState::Uninitialized => { - if !signers.contains(&self.unsigned_key()) { - return Err(InstructionError::MissingRequiredSignature); - } - (Lockup::default(), 0, false) // no lockup, no restrictions - } - _ => return Err(InstructionError::InvalidAccountData), - }; - - // verify that lockup has expired or that the withdrawal is signed by - // the custodian, both epoch and unix_timestamp must have passed - let custodian_pubkey = custodian.and_then(|keyed_account| keyed_account.signer_key()); - if lockup.is_in_force(&clock, custodian_pubkey) { - return Err(StakeError::LockupInForce.into()); - } - - // if the stake is active, we mustn't allow the account to go away - if is_staked // line coverage for branch coverage - && lamports + reserve > self.lamports()? - { - return Err(InstructionError::InsufficientFunds); - } - - if lamports != self.lamports()? // not a full withdrawal - && lamports + reserve > self.lamports()? - { - assert!(!is_staked); - return Err(InstructionError::InsufficientFunds); - } - - self.try_account_ref_mut()?.lamports -= lamports; - to.try_account_ref_mut()?.lamports += lamports; - Ok(()) - } -} - -#[derive(Clone, Debug, PartialEq)] -enum MergeKind { - Inactive(Meta, u64), - ActivationEpoch(Meta, Stake), - FullyActive(Meta, Stake), -} - -impl MergeKind { - fn meta(&self) -> &Meta { - match self { - Self::Inactive(meta, _) => meta, - Self::ActivationEpoch(meta, _) => meta, - Self::FullyActive(meta, _) => meta, - } - } - - fn active_stake(&self) -> Option<&Stake> { - match self { - Self::Inactive(_, _) => None, - Self::ActivationEpoch(_, stake) => Some(stake), - Self::FullyActive(_, stake) => Some(stake), - } - } - - fn get_if_mergeable( - stake_keyed_account: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - ) -> Result { - match stake_keyed_account.state()? { - StakeState::Stake(meta, stake) => { - // stake must not be in a transient state. Transient here meaning - // activating or deactivating with non-zero effective stake. - match stake.delegation.stake_activating_and_deactivating( - clock.epoch, - Some(stake_history), - true, - ) { - /* - (e, a, d): e - effective, a - activating, d - deactivating */ - (0, 0, 0) => Ok(Self::Inactive(meta, stake_keyed_account.lamports()?)), - (0, _, _) => Ok(Self::ActivationEpoch(meta, stake)), - (_, 0, 0) => Ok(Self::FullyActive(meta, stake)), - _ => Err(StakeError::MergeTransientStake.into()), - } - } - StakeState::Initialized(meta) => { - Ok(Self::Inactive(meta, stake_keyed_account.lamports()?)) - } - _ => Err(InstructionError::InvalidAccountData), - } - } - - fn metas_can_merge(stake: &Meta, source: &Meta) -> Result<(), InstructionError> { - // `rent_exempt_reserve` has no bearing on the mergeability of accounts, - // as the source account will be culled by runtime once the operation - // succeeds. Considering it here would needlessly prevent merging stake - // accounts with differing data lengths, which already exist in the wild - // due to an SDK bug - if stake.authorized == source.authorized && stake.lockup == source.lockup { - Ok(()) - } else { - Err(StakeError::MergeMismatch.into()) - } - } - - fn active_delegations_can_merge( - stake: &Delegation, - source: &Delegation, - ) -> Result<(), InstructionError> { - if stake.voter_pubkey == source.voter_pubkey - && (stake.warmup_cooldown_rate - source.warmup_cooldown_rate).abs() < f64::EPSILON - && stake.deactivation_epoch == Epoch::MAX - && source.deactivation_epoch == Epoch::MAX - { - Ok(()) - } else { - Err(StakeError::MergeMismatch.into()) - } - } - - fn active_stakes_can_merge(stake: &Stake, source: &Stake) -> Result<(), InstructionError> { - Self::active_delegations_can_merge(&stake.delegation, &source.delegation)?; - // `credits_observed` MUST match to prevent earning multiple rewards - // from a stake account by merging it into another stake account that - // is small enough to not be paid out every epoch. This would effectively - // reset the larger stake accounts `credits_observed` to that of the - // smaller account. - if stake.credits_observed == source.credits_observed { - Ok(()) - } else { - Err(StakeError::MergeMismatch.into()) - } - } - - fn merge(self, source: Self) -> Result, InstructionError> { - Self::metas_can_merge(self.meta(), source.meta())?; - self.active_stake() - .zip(source.active_stake()) - .map(|(stake, source)| Self::active_stakes_can_merge(stake, source)) - .unwrap_or(Ok(()))?; - let merged_state = match (self, source) { - (Self::Inactive(_, _), Self::Inactive(_, _)) => None, - (Self::Inactive(_, _), Self::ActivationEpoch(_, _)) => None, - (Self::ActivationEpoch(meta, mut stake), Self::Inactive(_, source_lamports)) => { - stake.delegation.stake += source_lamports; - Some(StakeState::Stake(meta, stake)) - } - ( - Self::ActivationEpoch(meta, mut stake), - Self::ActivationEpoch(source_meta, source_stake), - ) => { - let source_lamports = - source_meta.rent_exempt_reserve + source_stake.delegation.stake; - stake.delegation.stake += source_lamports; - Some(StakeState::Stake(meta, stake)) - } - (Self::FullyActive(meta, mut stake), Self::FullyActive(_, source_stake)) => { - // Don't stake the source account's `rent_exempt_reserve` to - // protect against the magic activation loophole. It will - // instead be moved into the destination account as extra, - // withdrawable `lamports` - stake.delegation.stake += source_stake.delegation.stake; - Some(StakeState::Stake(meta, stake)) - } - _ => return Err(StakeError::MergeMismatch.into()), - }; - Ok(merged_state) - } -} - -// utility function, used by Split -//This emulates current Rent math in order to preserve backward compatibility. In the future, and -//to support variable rent, the Split instruction should pass in the Rent sysvar instead. -fn calculate_split_rent_exempt_reserve( - source_rent_exempt_reserve: u64, - source_data_len: u64, - split_data_len: u64, -) -> u64 { - let lamports_per_byte_year = - source_rent_exempt_reserve / (source_data_len + ACCOUNT_STORAGE_OVERHEAD); - lamports_per_byte_year * (split_data_len + ACCOUNT_STORAGE_OVERHEAD) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - id, - stake_state::{new_stake_history_entry, null_tracer, rewrite_stakes}, - }; - use solana_sdk::{ - account::Account, native_token, pubkey::Pubkey, stake_history::StakeHistoryEntry, - system_program, - }; - use solana_vote_program::vote_state; - use std::{cell::RefCell, iter::FromIterator}; - - #[test] - fn test_authorized_authorize() { - let staker = solana_sdk::pubkey::new_rand(); - let mut authorized = Authorized::auto(&staker); - let mut signers = HashSet::new(); - assert_eq!( - authorized.authorize(&signers, &staker, StakeAuthorize::Staker), - Err(InstructionError::MissingRequiredSignature) - ); - signers.insert(staker); - assert_eq!( - authorized.authorize(&signers, &staker, StakeAuthorize::Staker), - Ok(()) - ); - } - - #[test] - fn test_stake_state_stake_from_fail() { - let mut stake_account = Account::new(0, std::mem::size_of::(), &id()); - - stake_account - .set_state(&StakeState::default()) - .expect("set_state"); - - assert_eq!(StakeState::stake_from(&stake_account), None); - } - - #[test] - fn test_stake_is_bootstrap() { - assert_eq!( - Delegation { - activation_epoch: std::u64::MAX, - ..Delegation::default() - } - .is_bootstrap(), - true - ); - assert_eq!( - Delegation { - activation_epoch: 0, - ..Delegation::default() - } - .is_bootstrap(), - false - ); - } - - #[test] - fn test_stake_delegate() { - let mut clock = Clock { - epoch: 1, - ..Clock::default() - }; - - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let mut vote_state = VoteState::default(); - for i in 0..1000 { - vote_state.process_slot_vote_unchecked(i); - } - - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - let vote_state_credits = vote_state.credits(); - vote_keyed_account - .set_state(&VoteStateVersions::new_current(vote_state)) - .unwrap(); - - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta { - authorized: Authorized { - staker: stake_pubkey, - withdrawer: stake_pubkey, - }, - ..Meta::default() - }), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - // unsigned keyed account - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - - let mut signers = HashSet::default(); - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // signed keyed account - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - signers.insert(stake_pubkey); - assert!(stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ) - .is_ok()); - - // verify that delegate() looks right, compare against hand-rolled - let stake = StakeState::stake_from(&stake_keyed_account.account.borrow()).unwrap(); - assert_eq!( - stake, - Stake { - delegation: Delegation { - voter_pubkey: vote_pubkey, - stake: stake_lamports, - activation_epoch: clock.epoch, - deactivation_epoch: std::u64::MAX, - ..Delegation::default() - }, - credits_observed: vote_state_credits, - } - ); - - clock.epoch += 1; - - // verify that delegate fails if stake is still active - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers - ), - Err(StakeError::TooSoonToRedelegate.into()) - ); - - // deactivate, so we can re-delegate - stake_keyed_account.deactivate(&clock, &signers).unwrap(); - - // without stake history, cool down is instantaneous - clock.epoch += 1; - - // verify that delegate can be called twice, 2nd is redelegate - assert!(stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers - ) - .is_ok()); - - // signed but faked vote account - let faked_vote_account = vote_account.clone(); - faked_vote_account.borrow_mut().owner = solana_sdk::pubkey::new_rand(); - let faked_vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &faked_vote_account); - assert_eq!( - stake_keyed_account.delegate( - &faked_vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Err(solana_sdk::instruction::InstructionError::IncorrectProgramId) - ); - - // verify that delegate() looks right, compare against hand-rolled - let stake = StakeState::stake_from(&stake_keyed_account.account.borrow()).unwrap(); - assert_eq!( - stake, - Stake { - delegation: Delegation { - voter_pubkey: vote_pubkey, - stake: stake_lamports, - activation_epoch: clock.epoch, - deactivation_epoch: std::u64::MAX, - ..Delegation::default() - }, - credits_observed: vote_state_credits, - } - ); - - // verify that non-stakes fail delegate() - let stake_state = StakeState::RewardsPool; - - stake_keyed_account.set_state(&stake_state).unwrap(); - assert!(stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers - ) - .is_err()); - } - - fn create_stake_history_from_delegations( - bootstrap: Option, - epochs: std::ops::Range, - delegations: &[Delegation], - ) -> StakeHistory { - let mut stake_history = StakeHistory::default(); - - let bootstrap_delegation = if let Some(bootstrap) = bootstrap { - vec![Delegation { - activation_epoch: std::u64::MAX, - stake: bootstrap, - ..Delegation::default() - }] - } else { - vec![] - }; - - for epoch in epochs { - let entry = new_stake_history_entry( - epoch, - delegations.iter().chain(bootstrap_delegation.iter()), - Some(&stake_history), - true, - ); - stake_history.add(epoch, entry); - } - - stake_history - } - - #[test] - fn test_stake_activating_and_deactivating() { - let stake = Delegation { - stake: 1_000, - activation_epoch: 0, // activating at zero - deactivation_epoch: 5, - ..Delegation::default() - }; - - // save this off so stake.config.warmup_rate changes don't break this test - let increment = (1_000_f64 * stake.warmup_cooldown_rate) as u64; - - let mut stake_history = StakeHistory::default(); - // assert that this stake follows step function if there's no history - assert_eq!( - stake.stake_activating_and_deactivating( - stake.activation_epoch, - Some(&stake_history), - true - ), - (0, stake.stake, 0) - ); - for epoch in stake.activation_epoch + 1..stake.deactivation_epoch { - assert_eq!( - stake.stake_activating_and_deactivating(epoch, Some(&stake_history), true), - (stake.stake, 0, 0) - ); - } - // assert that this stake is full deactivating - assert_eq!( - stake.stake_activating_and_deactivating( - stake.deactivation_epoch, - Some(&stake_history), - true - ), - (stake.stake, 0, stake.stake) - ); - // assert that this stake is fully deactivated if there's no history - assert_eq!( - stake.stake_activating_and_deactivating( - stake.deactivation_epoch + 1, - Some(&stake_history), - true, - ), - (0, 0, 0) - ); - - stake_history.add( - 0u64, // entry for zero doesn't have my activating amount - StakeHistoryEntry { - effective: 1_000, - activating: 0, - ..StakeHistoryEntry::default() - }, - ); - // assert that this stake is broken, because above setup is broken - assert_eq!( - stake.stake_activating_and_deactivating(1, Some(&stake_history), true), - (0, stake.stake, 0) - ); - - stake_history.add( - 0u64, // entry for zero has my activating amount - StakeHistoryEntry { - effective: 1_000, - activating: 1_000, - ..StakeHistoryEntry::default() - }, - // no entry for 1, so this stake gets shorted - ); - // assert that this stake is broken, because above setup is broken - assert_eq!( - stake.stake_activating_and_deactivating(2, Some(&stake_history), true), - (increment, stake.stake - increment, 0) - ); - - // start over, test deactivation edge cases - let mut stake_history = StakeHistory::default(); - - stake_history.add( - stake.deactivation_epoch, // entry for zero doesn't have my de-activating amount - StakeHistoryEntry { - effective: 1_000, - activating: 0, - ..StakeHistoryEntry::default() - }, - ); - // assert that this stake is broken, because above setup is broken - assert_eq!( - stake.stake_activating_and_deactivating( - stake.deactivation_epoch + 1, - Some(&stake_history), - true, - ), - (stake.stake, 0, stake.stake) // says "I'm still waiting for deactivation" - ); - - // put in my initial deactivating amount, but don't put in an entry for next - stake_history.add( - stake.deactivation_epoch, // entry for zero has my de-activating amount - StakeHistoryEntry { - effective: 1_000, - deactivating: 1_000, - ..StakeHistoryEntry::default() - }, - ); - // assert that this stake is broken, because above setup is broken - assert_eq!( - stake.stake_activating_and_deactivating( - stake.deactivation_epoch + 2, - Some(&stake_history), - true, - ), - (stake.stake - increment, 0, stake.stake - increment) // hung, should be lower - ); - } - - mod same_epoch_activation_then_deactivation { - use super::*; - - enum OldDeactivationBehavior { - Stuck, - Slow, - } - - fn do_test( - old_behavior: OldDeactivationBehavior, - fix_stake_deactivate: bool, - expected_stakes: &[(u64, u64, u64)], - ) { - let cluster_stake = 1_000; - let activating_stake = 10_000; - let some_stake = 700; - let some_epoch = 0; - - let stake = Delegation { - stake: some_stake, - activation_epoch: some_epoch, - deactivation_epoch: some_epoch, - ..Delegation::default() - }; - - let mut stake_history = StakeHistory::default(); - let cluster_deactivation_at_stake_modified_epoch = match old_behavior { - OldDeactivationBehavior::Stuck => 0, - OldDeactivationBehavior::Slow => 1000, - }; - - let stake_history_entries = vec![ - ( - cluster_stake, - activating_stake, - cluster_deactivation_at_stake_modified_epoch, - ), - (cluster_stake, activating_stake, 1000), - (cluster_stake, activating_stake, 1000), - (cluster_stake, activating_stake, 100), - (cluster_stake, activating_stake, 100), - (cluster_stake, activating_stake, 100), - (cluster_stake, activating_stake, 100), - ]; - - for (epoch, (effective, activating, deactivating)) in - stake_history_entries.into_iter().enumerate() - { - stake_history.add( - epoch as Epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - } - - assert_eq!( - expected_stakes, - (0..expected_stakes.len()) - .map(|epoch| stake.stake_activating_and_deactivating( - epoch as u64, - Some(&stake_history), - fix_stake_deactivate - )) - .collect::>() - ); - } - - #[test] - fn test_old_behavior_slow() { - do_test( - OldDeactivationBehavior::Slow, - false, - &[ - (0, 0, 0), - (13, 0, 13), - (10, 0, 10), - (8, 0, 8), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - ], - ); - } - - #[test] - fn test_old_behavior_stuck() { - do_test( - OldDeactivationBehavior::Stuck, - false, - &[ - (0, 0, 0), - (17, 0, 17), - (17, 0, 17), - (17, 0, 17), - (17, 0, 17), - (17, 0, 17), - (17, 0, 17), - ], - ); - } - - #[test] - fn test_new_behavior_previously_slow() { - // any stake accounts activated and deactivated at the same epoch - // shouldn't been activated (then deactivated) at all! - - do_test( - OldDeactivationBehavior::Slow, - true, - &[ - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - ], - ); - } - - #[test] - fn test_new_behavior_previously_stuck() { - // any stake accounts activated and deactivated at the same epoch - // shouldn't been activated (then deactivated) at all! - - do_test( - OldDeactivationBehavior::Stuck, - true, - &[ - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - ], - ); - } - } - - #[test] - fn test_inflation_and_slashing_with_activating_and_deactivating_stake() { - // some really boring delegation and stake_history setup - let (delegated_stake, mut stake, stake_history) = { - let cluster_stake = 1_000; - let delegated_stake = 700; - - let stake = Delegation { - stake: delegated_stake, - activation_epoch: 0, - deactivation_epoch: 4, - ..Delegation::default() - }; - - let mut stake_history = StakeHistory::default(); - stake_history.add( - 0, - StakeHistoryEntry { - effective: cluster_stake, - activating: delegated_stake, - ..StakeHistoryEntry::default() - }, - ); - let newly_effective_at_epoch1 = (cluster_stake as f64 * 0.25) as u64; - assert_eq!(newly_effective_at_epoch1, 250); - stake_history.add( - 1, - StakeHistoryEntry { - effective: cluster_stake + newly_effective_at_epoch1, - activating: delegated_stake - newly_effective_at_epoch1, - ..StakeHistoryEntry::default() - }, - ); - let newly_effective_at_epoch2 = - ((cluster_stake + newly_effective_at_epoch1) as f64 * 0.25) as u64; - assert_eq!(newly_effective_at_epoch2, 312); - stake_history.add( - 2, - StakeHistoryEntry { - effective: cluster_stake - + newly_effective_at_epoch1 - + newly_effective_at_epoch2, - activating: delegated_stake - - newly_effective_at_epoch1 - - newly_effective_at_epoch2, - ..StakeHistoryEntry::default() - }, - ); - stake_history.add( - 3, - StakeHistoryEntry { - effective: cluster_stake + delegated_stake, - ..StakeHistoryEntry::default() - }, - ); - stake_history.add( - 4, - StakeHistoryEntry { - effective: cluster_stake + delegated_stake, - deactivating: delegated_stake, - ..StakeHistoryEntry::default() - }, - ); - let newly_not_effective_stake_at_epoch5 = - ((cluster_stake + delegated_stake) as f64 * 0.25) as u64; - assert_eq!(newly_not_effective_stake_at_epoch5, 425); - stake_history.add( - 5, - StakeHistoryEntry { - effective: cluster_stake + delegated_stake - - newly_not_effective_stake_at_epoch5, - deactivating: delegated_stake - newly_not_effective_stake_at_epoch5, - ..StakeHistoryEntry::default() - }, - ); - - (delegated_stake, stake, stake_history) - }; - - // helper closures - let calculate_each_staking_status = |stake: &Delegation, epoch_count: usize| -> Vec<_> { - (0..epoch_count) - .map(|epoch| { - stake.stake_activating_and_deactivating( - epoch as u64, - Some(&stake_history), - true, - ) - }) - .collect::>() - }; - let adjust_staking_status = |rate: f64, status: &Vec<_>| { - status - .clone() - .into_iter() - .map(|(a, b, c)| { - ( - (a as f64 * rate) as u64, - (b as f64 * rate) as u64, - (c as f64 * rate) as u64, - ) - }) - .collect::>() - }; - - let expected_staking_status_transition = vec![ - (0, 700, 0), - (250, 450, 0), - (562, 138, 0), - (700, 0, 0), - (700, 0, 700), - (275, 0, 275), - (0, 0, 0), - ]; - let expected_staking_status_transition_base = vec![ - (0, 700, 0), - (250, 450, 0), - (562, 138 + 1, 0), // +1 is needed for rounding - (700, 0, 0), - (700, 0, 700), - (275 + 1, 0, 275 + 1), // +1 is needed for rounding - (0, 0, 0), - ]; - - // normal stake activating and deactivating transition test, just in case - assert_eq!( - expected_staking_status_transition, - calculate_each_staking_status(&stake, expected_staking_status_transition.len()) - ); - - // 10% inflation rewards assuming some sizable epochs passed! - let rate = 1.10; - stake.stake = (delegated_stake as f64 * rate) as u64; - let expected_staking_status_transition = - adjust_staking_status(rate, &expected_staking_status_transition_base); - - assert_eq!( - expected_staking_status_transition, - calculate_each_staking_status(&stake, expected_staking_status_transition_base.len()), - ); - - // 50% slashing!!! - let rate = 0.5; - stake.stake = (delegated_stake as f64 * rate) as u64; - let expected_staking_status_transition = - adjust_staking_status(rate, &expected_staking_status_transition_base); - - assert_eq!( - expected_staking_status_transition, - calculate_each_staking_status(&stake, expected_staking_status_transition_base.len()), - ); - } - - #[test] - fn test_stop_activating_after_deactivation() { - solana_logger::setup(); - let stake = Delegation { - stake: 1_000, - activation_epoch: 0, - deactivation_epoch: 3, - ..Delegation::default() - }; - - let base_stake = 1_000; - let mut stake_history = StakeHistory::default(); - let mut effective = base_stake; - let other_activation = 100; - let mut other_activations = vec![0]; - - // Build a stake history where the test staker always consumes all of the available warm - // up and cool down stake. However, simulate other stakers beginning to activate during - // the test staker's deactivation. - for epoch in 0..=stake.deactivation_epoch + 1 { - let (activating, deactivating) = if epoch < stake.deactivation_epoch { - (stake.stake + base_stake - effective, 0) - } else { - let other_activation_sum: u64 = other_activations.iter().sum(); - let deactivating = effective - base_stake - other_activation_sum; - (other_activation, deactivating) - }; - - stake_history.add( - epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - - if epoch < stake.deactivation_epoch { - let increase = (effective as f64 * stake.warmup_cooldown_rate) as u64; - effective += increase.min(activating); - other_activations.push(0); - } else { - let decrease = (effective as f64 * stake.warmup_cooldown_rate) as u64; - effective -= decrease.min(deactivating); - effective += other_activation; - other_activations.push(other_activation); - } - } - - for epoch in 0..=stake.deactivation_epoch + 1 { - let history = stake_history.get(&epoch).unwrap(); - let other_activations: u64 = other_activations[..=epoch as usize].iter().sum(); - let expected_stake = history.effective - base_stake - other_activations; - let (expected_activating, expected_deactivating) = if epoch < stake.deactivation_epoch { - (history.activating, 0) - } else { - (0, history.deactivating) - }; - assert_eq!( - stake.stake_activating_and_deactivating(epoch, Some(&stake_history), true), - (expected_stake, expected_activating, expected_deactivating) - ); - } - } - - #[test] - fn test_stake_warmup_cooldown_sub_integer_moves() { - let delegations = [Delegation { - stake: 2, - activation_epoch: 0, // activating at zero - deactivation_epoch: 5, - ..Delegation::default() - }]; - // give 2 epochs of cooldown - let epochs = 7; - // make boostrap stake smaller than warmup so warmup/cooldownn - // increment is always smaller than 1 - let bootstrap = (delegations[0].warmup_cooldown_rate * 100.0 / 2.0) as u64; - let stake_history = - create_stake_history_from_delegations(Some(bootstrap), 0..epochs, &delegations); - let mut max_stake = 0; - let mut min_stake = 2; - - for epoch in 0..epochs { - let stake = delegations - .iter() - .map(|delegation| delegation.stake(epoch, Some(&stake_history), true)) - .sum::(); - max_stake = max_stake.max(stake); - min_stake = min_stake.min(stake); - } - assert_eq!(max_stake, 2); - assert_eq!(min_stake, 0); - } - - #[test] - fn test_stake_warmup_cooldown() { - let delegations = [ - Delegation { - // never deactivates - stake: 1_000, - activation_epoch: std::u64::MAX, - ..Delegation::default() - }, - Delegation { - stake: 1_000, - activation_epoch: 0, - deactivation_epoch: 9, - ..Delegation::default() - }, - Delegation { - stake: 1_000, - activation_epoch: 1, - deactivation_epoch: 6, - ..Delegation::default() - }, - Delegation { - stake: 1_000, - activation_epoch: 2, - deactivation_epoch: 5, - ..Delegation::default() - }, - Delegation { - stake: 1_000, - activation_epoch: 2, - deactivation_epoch: 4, - ..Delegation::default() - }, - Delegation { - stake: 1_000, - activation_epoch: 4, - deactivation_epoch: 4, - ..Delegation::default() - }, - ]; - // chosen to ensure that the last activated stake (at 4) finishes - // warming up and cooling down - // a stake takes 2.0f64.log(1.0 + STAKE_WARMUP_RATE) epochs to warm up or cool down - // when all alone, but the above overlap a lot - let epochs = 20; - - let stake_history = create_stake_history_from_delegations(None, 0..epochs, &delegations); - - let mut prev_total_effective_stake = delegations - .iter() - .map(|delegation| delegation.stake(0, Some(&stake_history), true)) - .sum::(); - - // uncomment and add ! for fun with graphing - // eprintln("\n{:8} {:8} {:8}", " epoch", " total", " delta"); - for epoch in 1..epochs { - let total_effective_stake = delegations - .iter() - .map(|delegation| delegation.stake(epoch, Some(&stake_history), true)) - .sum::(); - - let delta = if total_effective_stake > prev_total_effective_stake { - total_effective_stake - prev_total_effective_stake - } else { - prev_total_effective_stake - total_effective_stake - }; - - // uncomment and add ! for fun with graphing - //eprint("{:8} {:8} {:8} ", epoch, total_effective_stake, delta); - //(0..(total_effective_stake as usize / (stakes.len() * 5))).for_each(|_| eprint("#")); - //eprintln(); - - assert!( - delta - <= ((prev_total_effective_stake as f64 * Config::default().warmup_cooldown_rate) as u64) - .max(1) - ); - - prev_total_effective_stake = total_effective_stake; - } - } - - #[test] - fn test_stake_initialize() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = - Account::new_ref(stake_lamports, std::mem::size_of::(), &id()); - - // unsigned keyed account - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - let custodian = solana_sdk::pubkey::new_rand(); - - // not enough balance for rent... - assert_eq!( - stake_keyed_account.initialize( - &Authorized::default(), - &Lockup::default(), - &Rent { - lamports_per_byte_year: 42, - ..Rent::free() - }, - ), - Err(InstructionError::InsufficientFunds) - ); - - // this one works, as is uninit - assert_eq!( - stake_keyed_account.initialize( - &Authorized::auto(&stake_pubkey), - &Lockup { - epoch: 1, - unix_timestamp: 0, - custodian - }, - &Rent::free(), - ), - Ok(()) - ); - // check that we see what we expect - assert_eq!( - StakeState::from(&stake_keyed_account.account.borrow()).unwrap(), - StakeState::Initialized(Meta { - lockup: Lockup { - unix_timestamp: 0, - epoch: 1, - custodian - }, - ..Meta { - authorized: Authorized::auto(&stake_pubkey), - ..Meta::default() - } - }) - ); - - // 2nd time fails, can't move it from anything other than uninit->init - assert_eq!( - stake_keyed_account.initialize( - &Authorized::default(), - &Lockup::default(), - &Rent::free() - ), - Err(InstructionError::InvalidAccountData) - ); - } - - #[test] - fn test_initialize_incorrect_account_sizes() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = - Account::new_ref(stake_lamports, std::mem::size_of::() + 1, &id()); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - - assert_eq!( - stake_keyed_account.initialize( - &Authorized::default(), - &Lockup::default(), - &Rent { - lamports_per_byte_year: 42, - ..Rent::free() - }, - ), - Err(InstructionError::InvalidAccountData) - ); - - let stake_account = - Account::new_ref(stake_lamports, std::mem::size_of::() - 1, &id()); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - - assert_eq!( - stake_keyed_account.initialize( - &Authorized::default(), - &Lockup::default(), - &Rent { - lamports_per_byte_year: 42, - ..Rent::free() - }, - ), - Err(InstructionError::InvalidAccountData) - ); - } - - #[test] - fn test_deactivate() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta::auto(&stake_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let clock = Clock { - epoch: 1, - ..Clock::default() - }; - - // signed keyed account but not staked yet - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let signers = vec![stake_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.deactivate(&clock, &signers), - Err(InstructionError::InvalidAccountData) - ); - - // Staking - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - vote_keyed_account - .set_state(&VoteStateVersions::new_current(VoteState::default())) - .unwrap(); - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers - ), - Ok(()) - ); - - // no signers fails - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - assert_eq!( - stake_keyed_account.deactivate(&clock, &HashSet::default()), - Err(InstructionError::MissingRequiredSignature) - ); - - // Deactivate after staking - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - assert_eq!(stake_keyed_account.deactivate(&clock, &signers), Ok(())); - - // verify that deactivate() only works once - assert_eq!( - stake_keyed_account.deactivate(&clock, &signers), - Err(StakeError::AlreadyDeactivated.into()) - ); - } - - #[test] - fn test_set_lockup() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - // wrong state, should fail - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - assert_eq!( - stake_keyed_account.set_lockup(&LockupArgs::default(), &HashSet::default()), - Err(InstructionError::InvalidAccountData) - ); - - // initalize the stake - let custodian = solana_sdk::pubkey::new_rand(); - stake_keyed_account - .initialize( - &Authorized::auto(&stake_pubkey), - &Lockup { - unix_timestamp: 1, - epoch: 1, - custodian, - }, - &Rent::free(), - ) - .unwrap(); - - assert_eq!( - stake_keyed_account.set_lockup(&LockupArgs::default(), &HashSet::default()), - Err(InstructionError::MissingRequiredSignature) - ); - - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: Some(1), - epoch: Some(1), - custodian: Some(custodian), - }, - &vec![custodian].into_iter().collect() - ), - Ok(()) - ); - - // delegate stake - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - vote_keyed_account - .set_state(&VoteStateVersions::new_current(VoteState::default())) - .unwrap(); - - stake_keyed_account - .delegate( - &vote_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &Config::default(), - &vec![stake_pubkey].into_iter().collect(), - ) - .unwrap(); - - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: Some(1), - epoch: Some(1), - custodian: Some(custodian), - }, - &HashSet::default(), - ), - Err(InstructionError::MissingRequiredSignature) - ); - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: Some(1), - epoch: Some(1), - custodian: Some(custodian), - }, - &vec![custodian].into_iter().collect() - ), - Ok(()) - ); - } - - #[test] - fn test_optional_lockup() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - - let custodian = solana_sdk::pubkey::new_rand(); - stake_keyed_account - .initialize( - &Authorized::auto(&stake_pubkey), - &Lockup { - unix_timestamp: 1, - epoch: 1, - custodian, - }, - &Rent::free(), - ) - .unwrap(); - - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: None, - epoch: None, - custodian: None, - }, - &vec![custodian].into_iter().collect() - ), - Ok(()) - ); - - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: Some(2), - epoch: None, - custodian: None, - }, - &vec![custodian].into_iter().collect() - ), - Ok(()) - ); - - if let StakeState::Initialized(Meta { lockup, .. }) = - StakeState::from(&stake_keyed_account.account.borrow()).unwrap() - { - assert_eq!(lockup.unix_timestamp, 2); - assert_eq!(lockup.epoch, 1); - assert_eq!(lockup.custodian, custodian); - } else { - panic!(); - } - - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: None, - epoch: Some(3), - custodian: None, - }, - &vec![custodian].into_iter().collect() - ), - Ok(()) - ); - - if let StakeState::Initialized(Meta { lockup, .. }) = - StakeState::from(&stake_keyed_account.account.borrow()).unwrap() - { - assert_eq!(lockup.unix_timestamp, 2); - assert_eq!(lockup.epoch, 3); - assert_eq!(lockup.custodian, custodian); - } else { - panic!(); - } - - let new_custodian = solana_sdk::pubkey::new_rand(); - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs { - unix_timestamp: None, - epoch: None, - custodian: Some(new_custodian), - }, - &vec![custodian].into_iter().collect() - ), - Ok(()) - ); - - if let StakeState::Initialized(Meta { lockup, .. }) = - StakeState::from(&stake_keyed_account.account.borrow()).unwrap() - { - assert_eq!(lockup.unix_timestamp, 2); - assert_eq!(lockup.epoch, 3); - assert_eq!(lockup.custodian, new_custodian); - } else { - panic!(); - } - - assert_eq!( - stake_keyed_account.set_lockup( - &LockupArgs::default(), - &vec![custodian].into_iter().collect() - ), - Err(InstructionError::MissingRequiredSignature) - ); - } - - #[test] - fn test_withdraw_stake() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let mut clock = Clock::default(); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = Account::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - // no signers, should fail - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &to_keyed_account, // unsigned account as withdraw authority - None, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // signed keyed account and uninitialized should work - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Ok(()) - ); - assert_eq!(stake_account.borrow().lamports, 0); - - // reset balance - stake_account.borrow_mut().lamports = stake_lamports; - - // lockup - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let custodian = solana_sdk::pubkey::new_rand(); - stake_keyed_account - .initialize( - &Authorized::auto(&stake_pubkey), - &Lockup { - unix_timestamp: 0, - epoch: 0, - custodian, - }, - &Rent::free(), - ) - .unwrap(); - - // signed keyed account and locked up, more than available should fail - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports + 1, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - - // Stake some lamports (available lamports for withdrawals will reduce to zero) - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - vote_keyed_account - .set_state(&VoteStateVersions::new_current(VoteState::default())) - .unwrap(); - let signers = vec![stake_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Ok(()) - ); - - // simulate rewards - stake_account.borrow_mut().lamports += 10; - // withdrawal before deactivate works for rewards amount - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - 10, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Ok(()) - ); - - // simulate rewards - stake_account.borrow_mut().lamports += 10; - // withdrawal of rewards fails if not in excess of stake - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - 10 + 1, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - - // deactivate the stake before withdrawal - assert_eq!(stake_keyed_account.deactivate(&clock, &signers), Ok(())); - // simulate time passing - clock.epoch += 100; - - // Try to withdraw more than what's available - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports + 10 + 1, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - - // Try to withdraw all lamports - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports + 10, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Ok(()) - ); - assert_eq!(stake_account.borrow().lamports, 0); - } - - #[test] - fn test_withdraw_stake_before_warmup() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let total_lamports = 100; - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - total_lamports, - &StakeState::Initialized(Meta::auto(&stake_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let clock = Clock::default(); - let mut future = Clock::default(); - future.epoch += 16; - - let to = solana_sdk::pubkey::new_rand(); - let to_account = Account::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - // Stake some lamports (available lamports for withdrawals will reduce) - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - vote_keyed_account - .set_state(&VoteStateVersions::new_current(VoteState::default())) - .unwrap(); - let signers = vec![stake_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.delegate( - &vote_keyed_account, - &future, - &StakeHistory::default(), - &Config::default(), - &signers, - ), - Ok(()) - ); - - let stake_history = create_stake_history_from_delegations( - None, - 0..future.epoch, - &[ - StakeState::stake_from(&stake_keyed_account.account.borrow()) - .unwrap() - .delegation, - ], - ); - - // Try to withdraw stake - assert_eq!( - stake_keyed_account.withdraw( - total_lamports - stake_lamports + 1, - &to_keyed_account, - &clock, - &stake_history, - &stake_keyed_account, - None, - ), - Err(InstructionError::InsufficientFunds) - ); - } - - #[test] - fn test_withdraw_stake_invalid_state() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let total_lamports = 100; - let stake_account = Account::new_ref_data_with_space( - total_lamports, - &StakeState::RewardsPool, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = Account::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(InstructionError::InvalidAccountData) - ); - } - - #[test] - fn test_withdraw_lockup() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let custodian = solana_sdk::pubkey::new_rand(); - let total_lamports = 100; - let stake_account = Account::new_ref_data_with_space( - total_lamports, - &StakeState::Initialized(Meta { - lockup: Lockup { - unix_timestamp: 0, - epoch: 1, - custodian, - }, - ..Meta::auto(&stake_pubkey) - }), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = Account::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - let mut clock = Clock::default(); - - // lockup is still in force, can't withdraw - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(StakeError::LockupInForce.into()) - ); - - { - let custodian_account = Account::new_ref(1, 0, &system_program::id()); - let custodian_keyed_account = KeyedAccount::new(&custodian, true, &custodian_account); - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - Some(&custodian_keyed_account), - ), - Ok(()) - ); - } - // reset balance - stake_keyed_account.account.borrow_mut().lamports = total_lamports; - - // lockup has expired - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - clock.epoch += 1; - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Ok(()) - ); - } - - #[test] - fn test_withdraw_identical_authorities() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let custodian = stake_pubkey; - let total_lamports = 100; - let stake_account = Account::new_ref_data_with_space( - total_lamports, - &StakeState::Initialized(Meta { - lockup: Lockup { - unix_timestamp: 0, - epoch: 1, - custodian, - }, - ..Meta::auto(&stake_pubkey) - }), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = Account::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - let clock = Clock::default(); - - // lockup is still in force, even though custodian is the same as the withdraw authority - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - None, - ), - Err(StakeError::LockupInForce.into()) - ); - - { - let custodian_keyed_account = KeyedAccount::new(&custodian, true, &stake_account); - assert_eq!( - stake_keyed_account.withdraw( - total_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, - Some(&custodian_keyed_account), - ), - Ok(()) - ); - } - } - - #[test] - fn test_stake_state_redeem_rewards() { - let mut vote_state = VoteState::default(); - // assume stake.stake() is right - // bootstrap means fully-vested stake at epoch 0 - let stake_lamports = 1; - let mut stake = Stake::new( - stake_lamports, - &Pubkey::default(), - &vote_state, - std::u64::MAX, - &Config::default(), - ); - - // this one can't collect now, credits_observed == vote_state.credits() - assert_eq!( - None, - stake.redeem_rewards( - &PointValue { - rewards: 1_000_000_000, - points: 1 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - // put 2 credits in at epoch 0 - vote_state.increment_credits(0); - vote_state.increment_credits(0); - - // this one should be able to collect exactly 2 - assert_eq!( - Some((stake_lamports * 2, 0)), - stake.redeem_rewards( - &PointValue { - rewards: 1, - points: 1 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - assert_eq!( - stake.delegation.stake, - stake_lamports + (stake_lamports * 2) - ); - assert_eq!(stake.credits_observed, 2); - } - - #[test] - fn test_stake_state_calculate_points_with_typical_values() { - let mut vote_state = VoteState::default(); - - // bootstrap means fully-vested stake at epoch 0 with - // 10_000_000 SOL is a big but not unreasaonable stake - let stake = Stake::new( - native_token::sol_to_lamports(10_000_000f64), - &Pubkey::default(), - &vote_state, - std::u64::MAX, - &Config::default(), - ); - - // this one can't collect now, credits_observed == vote_state.credits() - assert_eq!( - None, - stake.calculate_rewards( - &PointValue { - rewards: 1_000_000_000, - points: 1 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - let epoch_slots: u128 = 14 * 24 * 3600 * 160; - // put 193,536,000 credits in at epoch 0, typical for a 14-day epoch - // this loop takes a few seconds... - for _ in 0..epoch_slots { - vote_state.increment_credits(0); - } - - // no overflow on points - assert_eq!( - u128::from(stake.delegation.stake) * epoch_slots, - stake.calculate_points(&vote_state, None, &mut null_tracer(), true) - ); - } - - #[test] - fn test_stake_state_calculate_rewards() { - let mut vote_state = VoteState::default(); - // assume stake.stake() is right - // bootstrap means fully-vested stake at epoch 0 - let mut stake = Stake::new( - 1, - &Pubkey::default(), - &vote_state, - std::u64::MAX, - &Config::default(), - ); - - // this one can't collect now, credits_observed == vote_state.credits() - assert_eq!( - None, - stake.calculate_rewards( - &PointValue { - rewards: 1_000_000_000, - points: 1 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - // put 2 credits in at epoch 0 - vote_state.increment_credits(0); - vote_state.increment_credits(0); - - // this one should be able to collect exactly 2 - assert_eq!( - Some((stake.delegation.stake * 2, 0, 2)), - stake.calculate_rewards( - &PointValue { - rewards: 2, - points: 2 // all his - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - stake.credits_observed = 1; - // this one should be able to collect exactly 1 (already observed one) - assert_eq!( - Some((stake.delegation.stake, 0, 2)), - stake.calculate_rewards( - &PointValue { - rewards: 1, - points: 1 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - // put 1 credit in epoch 1 - vote_state.increment_credits(1); - - stake.credits_observed = 2; - // this one should be able to collect the one just added - assert_eq!( - Some((stake.delegation.stake, 0, 3)), - stake.calculate_rewards( - &PointValue { - rewards: 2, - points: 2 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - // put 1 credit in epoch 2 - vote_state.increment_credits(2); - // this one should be able to collect 2 now - assert_eq!( - Some((stake.delegation.stake * 2, 0, 4)), - stake.calculate_rewards( - &PointValue { - rewards: 2, - points: 2 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - stake.credits_observed = 0; - // this one should be able to collect everything from t=0 a warmed up stake of 2 - // (2 credits at stake of 1) + (1 credit at a stake of 2) - assert_eq!( - Some(( - stake.delegation.stake * 2 // epoch 0 - + stake.delegation.stake // epoch 1 - + stake.delegation.stake, // epoch 2 - 0, - 4 - )), - stake.calculate_rewards( - &PointValue { - rewards: 4, - points: 4 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - // same as above, but is a really small commission out of 32 bits, - // verify that None comes back on small redemptions where no one gets paid - vote_state.commission = 1; - assert_eq!( - None, // would be Some((0, 2 * 1 + 1 * 2, 4)), - stake.calculate_rewards( - &PointValue { - rewards: 4, - points: 4 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - vote_state.commission = 99; - assert_eq!( - None, // would be Some((0, 2 * 1 + 1 * 2, 4)), - stake.calculate_rewards( - &PointValue { - rewards: 4, - points: 4 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - // now one with inflation disabled. no one gets paid, but we still need - // to advance the stake state's credits_observed field to prevent back- - // paying rewards when inflation is turned on. - assert_eq!( - Some((0, 0, 4)), - stake.calculate_rewards( - &PointValue { - rewards: 0, - points: 4 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - // credits_observed remains at previous level when vote_state credits are - // not advancing and inflation is disabled - stake.credits_observed = 4; - assert_eq!( - Some((0, 0, 4)), - stake.calculate_rewards( - &PointValue { - rewards: 0, - points: 4 - }, - &vote_state, - None, - &mut null_tracer(), - true, - ) - ); - - // assert the previous behavior is preserved where fix_stake_deactivate=false - assert_eq!( - (0, 0), - stake.calculate_points_and_credits(&vote_state, None, &mut null_tracer(), false) - ); - assert_eq!( - (0, 4), - stake.calculate_points_and_credits(&vote_state, None, &mut null_tracer(), true) - ); - } - - #[test] - fn test_authorize_uninit() { - let new_authority = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::default(), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let stake_keyed_account = KeyedAccount::new(&new_authority, true, &stake_account); - let signers = vec![new_authority].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize(&signers, &new_authority, StakeAuthorize::Staker), - Err(InstructionError::InvalidAccountData) - ); - } - - #[test] - fn test_authorize_lockup() { - let stake_authority = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta::auto(&stake_authority)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let to = solana_sdk::pubkey::new_rand(); - let to_account = Account::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - - let clock = Clock::default(); - let stake_keyed_account = KeyedAccount::new(&stake_authority, true, &stake_account); - - let stake_pubkey0 = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_authority].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize(&signers, &stake_pubkey0, StakeAuthorize::Staker), - Ok(()) - ); - assert_eq!( - stake_keyed_account.authorize(&signers, &stake_pubkey0, StakeAuthorize::Withdrawer), - Ok(()) - ); - if let StakeState::Initialized(Meta { authorized, .. }) = - StakeState::from(&stake_keyed_account.account.borrow()).unwrap() - { - assert_eq!(authorized.staker, stake_pubkey0); - assert_eq!(authorized.withdrawer, stake_pubkey0); - } else { - panic!(); - } - - // A second authorization signed by the stake_keyed_account should fail - let stake_pubkey1 = solana_sdk::pubkey::new_rand(); - assert_eq!( - stake_keyed_account.authorize(&signers, &stake_pubkey1, StakeAuthorize::Staker), - Err(InstructionError::MissingRequiredSignature) - ); - - let signers0 = vec![stake_pubkey0].into_iter().collect(); - - // Test a second authorization by the newly authorized pubkey - let stake_pubkey2 = solana_sdk::pubkey::new_rand(); - assert_eq!( - stake_keyed_account.authorize(&signers0, &stake_pubkey2, StakeAuthorize::Staker), - Ok(()) - ); - if let StakeState::Initialized(Meta { authorized, .. }) = - StakeState::from(&stake_keyed_account.account.borrow()).unwrap() - { - assert_eq!(authorized.staker, stake_pubkey2); - } - - assert_eq!( - stake_keyed_account.authorize(&signers0, &stake_pubkey2, StakeAuthorize::Withdrawer), - Ok(()) - ); - if let StakeState::Initialized(Meta { authorized, .. }) = - StakeState::from(&stake_keyed_account.account.borrow()).unwrap() - { - assert_eq!(authorized.staker, stake_pubkey2); - } - - // Test that withdrawal to account fails without authorized withdrawer - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account, // old signer - None, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - let stake_keyed_account2 = KeyedAccount::new(&stake_pubkey2, true, &stake_account); - - // Test a successful action by the currently authorized withdrawer - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - assert_eq!( - stake_keyed_account.withdraw( - stake_lamports, - &to_keyed_account, - &clock, - &StakeHistory::default(), - &stake_keyed_account2, - None, - ), - Ok(()) - ); - } - - #[test] - fn test_authorize_with_seed() { - let base_pubkey = solana_sdk::pubkey::new_rand(); - let seed = "42"; - let withdrawer_pubkey = Pubkey::create_with_seed(&base_pubkey, &seed, &id()).unwrap(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta::auto(&withdrawer_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let base_account = Account::new_ref(1, 0, &id()); - let base_keyed_account = KeyedAccount::new(&base_pubkey, true, &base_account); - - let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account); - - let new_authority = solana_sdk::pubkey::new_rand(); - - // Wrong seed - assert_eq!( - stake_keyed_account.authorize_with_seed( - &base_keyed_account, - &"", - &id(), - &new_authority, - StakeAuthorize::Staker, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // Wrong base - assert_eq!( - stake_keyed_account.authorize_with_seed( - &stake_keyed_account, - &seed, - &id(), - &new_authority, - StakeAuthorize::Staker, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // Set stake authority - assert_eq!( - stake_keyed_account.authorize_with_seed( - &base_keyed_account, - &seed, - &id(), - &new_authority, - StakeAuthorize::Staker, - ), - Ok(()) - ); - - // Set withdraw authority - assert_eq!( - stake_keyed_account.authorize_with_seed( - &base_keyed_account, - &seed, - &id(), - &new_authority, - StakeAuthorize::Withdrawer, - ), - Ok(()) - ); - - // No longer withdraw authority - assert_eq!( - stake_keyed_account.authorize_with_seed( - &stake_keyed_account, - &seed, - &id(), - &new_authority, - StakeAuthorize::Withdrawer, - ), - Err(InstructionError::MissingRequiredSignature) - ); - } - - #[test] - fn test_authorize_override() { - let withdrawer_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta::auto(&withdrawer_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account); - - // Authorize a staker pubkey and move the withdrawer key into cold storage. - let new_authority = solana_sdk::pubkey::new_rand(); - let signers = vec![withdrawer_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize(&signers, &new_authority, StakeAuthorize::Staker), - Ok(()) - ); - - // Attack! The stake key (a hot key) is stolen and used to authorize a new staker. - let mallory_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![new_authority].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize(&signers, &mallory_pubkey, StakeAuthorize::Staker), - Ok(()) - ); - - // Verify the original staker no longer has access. - let new_stake_pubkey = solana_sdk::pubkey::new_rand(); - assert_eq!( - stake_keyed_account.authorize(&signers, &new_stake_pubkey, StakeAuthorize::Staker), - Err(InstructionError::MissingRequiredSignature) - ); - - // Verify the withdrawer (pulled from cold storage) can save the day. - let signers = vec![withdrawer_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize(&signers, &new_stake_pubkey, StakeAuthorize::Withdrawer), - Ok(()) - ); - - // Attack! Verify the staker cannot be used to authorize a withdraw. - let signers = vec![new_stake_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.authorize(&signers, &mallory_pubkey, StakeAuthorize::Withdrawer), - Ok(()) - ); - } - - #[test] - fn test_split_source_uninitialized() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let split_stake_account = Account::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, false, &split_stake_account); - - // no signers should fail - assert_eq!( - stake_keyed_account.split( - stake_lamports / 2, - &split_stake_keyed_account, - &HashSet::default() // no signers - ), - Err(InstructionError::MissingRequiredSignature) - ); - - // this should work - let signers = vec![stake_pubkey].into_iter().collect(); - assert_eq!( - stake_keyed_account.split(stake_lamports / 2, &split_stake_keyed_account, &signers), - Ok(()) - ); - assert_eq!( - stake_keyed_account.account.borrow().lamports, - split_stake_keyed_account.account.borrow().lamports - ); - } - - #[test] - fn test_split_split_not_uninitialized() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Stake(Meta::auto(&stake_pubkey), Stake::just_stake(stake_lamports)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let split_stake_account = Account::new_ref_data_with_space( - 0, - &StakeState::Initialized(Meta::auto(&stake_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let signers = vec![stake_pubkey].into_iter().collect(); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - assert_eq!( - stake_keyed_account.split(stake_lamports / 2, &split_stake_keyed_account, &signers), - Err(InstructionError::InvalidAccountData) - ); - } - impl Stake { - fn just_stake(stake: u64) -> Self { - Self { - delegation: Delegation { - stake, - ..Delegation::default() - }, - ..Stake::default() - } - } - } - - #[test] - fn test_split_more_than_staked() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Stake( - Meta::auto(&stake_pubkey), - Stake::just_stake(stake_lamports / 2 - 1), - ), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let split_stake_account = Account::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let signers = vec![stake_pubkey].into_iter().collect(); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - assert_eq!( - stake_keyed_account.split(stake_lamports / 2, &split_stake_keyed_account, &signers), - Err(StakeError::InsufficientStake.into()) - ); - } - - #[test] - fn test_split_with_rent() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 10_000_000; - let rent_exempt_reserve = 2_282_880; - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - // test splitting both an Initialized stake and a Staked stake - for state in &[ - StakeState::Initialized(meta), - StakeState::Stake( - meta, - Stake::just_stake(stake_lamports - rent_exempt_reserve), - ), - ] { - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - let split_stake_account = Account::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - // not enough to make a non-zero stake account - assert_eq!( - stake_keyed_account.split( - rent_exempt_reserve, - &split_stake_keyed_account, - &signers - ), - Err(InstructionError::InsufficientFunds) - ); - - // doesn't leave enough for initial stake to be non-zero - assert_eq!( - stake_keyed_account.split( - stake_lamports - rent_exempt_reserve, - &split_stake_keyed_account, - &signers - ), - Err(InstructionError::InsufficientFunds) - ); - - // split account already has way enough lamports - split_stake_keyed_account.account.borrow_mut().lamports = 10_000_000; - assert_eq!( - stake_keyed_account.split( - stake_lamports - (rent_exempt_reserve + 1), // leave rent_exempt_reserve + 1 in original account - &split_stake_keyed_account, - &signers - ), - Ok(()) - ); - - // verify no stake leakage in the case of a stake - if let StakeState::Stake(meta, stake) = state { - assert_eq!( - split_stake_keyed_account.state(), - Ok(StakeState::Stake( - *meta, - Stake { - delegation: Delegation { - stake: stake_lamports - rent_exempt_reserve - 1, - ..stake.delegation - }, - ..*stake - } - )) - ); - assert_eq!( - stake_keyed_account.account.borrow().lamports, - rent_exempt_reserve + 1 - ); - assert_eq!( - split_stake_keyed_account.account.borrow().lamports, - 10_000_000 + stake_lamports - rent_exempt_reserve - 1 - ); - } - } - } - - #[test] - fn test_split() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - // test splitting both an Initialized stake and a Staked stake - for state in &[ - StakeState::Initialized(Meta::auto(&stake_pubkey)), - StakeState::Stake(Meta::auto(&stake_pubkey), Stake::just_stake(stake_lamports)), - ] { - let split_stake_account = Account::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - // split more than available fails - assert_eq!( - stake_keyed_account.split(stake_lamports + 1, &split_stake_keyed_account, &signers), - Err(InstructionError::InsufficientFunds) - ); - - // should work - assert_eq!( - stake_keyed_account.split(stake_lamports / 2, &split_stake_keyed_account, &signers), - Ok(()) - ); - // no lamport leakage - assert_eq!( - stake_keyed_account.account.borrow().lamports - + split_stake_keyed_account.account.borrow().lamports, - stake_lamports - ); - - match state { - StakeState::Initialized(_) => { - assert_eq!(Ok(*state), split_stake_keyed_account.state()); - assert_eq!(Ok(*state), stake_keyed_account.state()); - } - StakeState::Stake(meta, stake) => { - assert_eq!( - Ok(StakeState::Stake( - *meta, - Stake { - delegation: Delegation { - stake: stake_lamports / 2, - ..stake.delegation - }, - ..*stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!( - Ok(StakeState::Stake( - *meta, - Stake { - delegation: Delegation { - stake: stake_lamports / 2, - ..stake.delegation - }, - ..*stake - } - )), - stake_keyed_account.state() - ); - } - _ => unreachable!(), - } - - // reset - stake_keyed_account.account.borrow_mut().lamports = stake_lamports; - } - } - - #[test] - fn test_split_fake_stake_dest() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let split_stake_account = Account::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &solana_sdk::pubkey::new_rand(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Stake(Meta::auto(&stake_pubkey), Stake::just_stake(stake_lamports)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - assert_eq!( - stake_keyed_account.split(stake_lamports / 2, &split_stake_keyed_account, &signers), - Err(InstructionError::IncorrectProgramId), - ); - } - - #[test] - fn test_split_to_account_with_rent_exempt_reserve() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake_lamports = rent_exempt_reserve * 3; // Enough to allow half to be split and remain rent-exempt - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - let state = StakeState::Stake( - meta, - Stake::just_stake(stake_lamports - rent_exempt_reserve), - ); - // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly - // rent_exempt_reserve, and more than rent_exempt_reserve. The empty case is not covered in - // test_split, since that test uses a Meta with rent_exempt_reserve = 0 - let split_lamport_balances = vec![0, 1, rent_exempt_reserve, rent_exempt_reserve + 1]; - for initial_balance in split_lamport_balances { - let split_stake_account = Account::new_ref_data_with_space( - initial_balance, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - // split more than available fails - assert_eq!( - stake_keyed_account.split(stake_lamports + 1, &split_stake_keyed_account, &signers), - Err(InstructionError::InsufficientFunds) - ); - - // should work - assert_eq!( - stake_keyed_account.split(stake_lamports / 2, &split_stake_keyed_account, &signers), - Ok(()) - ); - // no lamport leakage - assert_eq!( - stake_keyed_account.account.borrow().lamports - + split_stake_keyed_account.account.borrow().lamports, - stake_lamports + initial_balance - ); - - if let StakeState::Stake(meta, stake) = state { - let expected_stake = - stake_lamports / 2 - (rent_exempt_reserve.saturating_sub(initial_balance)); - assert_eq!( - Ok(StakeState::Stake( - meta, - Stake { - delegation: Delegation { - stake: stake_lamports / 2 - - (rent_exempt_reserve.saturating_sub(initial_balance)), - ..stake.delegation - }, - ..stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!( - split_stake_keyed_account.account.borrow().lamports, - expected_stake - + rent_exempt_reserve - + initial_balance.saturating_sub(rent_exempt_reserve) - ); - assert_eq!( - Ok(StakeState::Stake( - meta, - Stake { - delegation: Delegation { - stake: stake_lamports / 2 - rent_exempt_reserve, - ..stake.delegation - }, - ..stake - } - )), - stake_keyed_account.state() - ); - } - } - } - - #[test] - fn test_split_to_smaller_account_with_rent_exempt_reserve() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake_lamports = rent_exempt_reserve * 3; // Enough to allow half to be split and remain rent-exempt - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - let state = StakeState::Stake( - meta, - Stake::just_stake(stake_lamports - rent_exempt_reserve), - ); - - let expected_rent_exempt_reserve = calculate_split_rent_exempt_reserve( - meta.rent_exempt_reserve, - std::mem::size_of::() as u64 + 100, - std::mem::size_of::() as u64, - ); - - // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly - // rent_exempt_reserve, and more than rent_exempt_reserve. The empty case is not covered in - // test_split, since that test uses a Meta with rent_exempt_reserve = 0 - let split_lamport_balances = vec![ - 0, - 1, - expected_rent_exempt_reserve, - expected_rent_exempt_reserve + 1, - ]; - for initial_balance in split_lamport_balances { - let split_stake_account = Account::new_ref_data_with_space( - initial_balance, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &state, - std::mem::size_of::() + 100, - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - // split more than available fails - assert_eq!( - stake_keyed_account.split(stake_lamports + 1, &split_stake_keyed_account, &signers), - Err(InstructionError::InsufficientFunds) - ); - - // should work - assert_eq!( - stake_keyed_account.split(stake_lamports / 2, &split_stake_keyed_account, &signers), - Ok(()) - ); - // no lamport leakage - assert_eq!( - stake_keyed_account.account.borrow().lamports - + split_stake_keyed_account.account.borrow().lamports, - stake_lamports + initial_balance - ); - - if let StakeState::Stake(meta, stake) = state { - let expected_split_meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve: expected_rent_exempt_reserve, - ..Meta::default() - }; - let expected_stake = stake_lamports / 2 - - (expected_rent_exempt_reserve.saturating_sub(initial_balance)); - - assert_eq!( - Ok(StakeState::Stake( - expected_split_meta, - Stake { - delegation: Delegation { - stake: expected_stake, - ..stake.delegation - }, - ..stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!( - split_stake_keyed_account.account.borrow().lamports, - expected_stake - + expected_rent_exempt_reserve - + initial_balance.saturating_sub(expected_rent_exempt_reserve) - ); - assert_eq!( - Ok(StakeState::Stake( - meta, - Stake { - delegation: Delegation { - stake: stake_lamports / 2 - rent_exempt_reserve, - ..stake.delegation - }, - ..stake - } - )), - stake_keyed_account.state() - ); - } - } - } - - #[test] - fn test_split_to_larger_account_edge_case() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - let expected_rent_exempt_reserve = calculate_split_rent_exempt_reserve( - meta.rent_exempt_reserve, - std::mem::size_of::() as u64, - std::mem::size_of::() as u64 + 100, - ); - let stake_lamports = expected_rent_exempt_reserve + 1; - let split_amount = stake_lamports - (rent_exempt_reserve + 1); // Enough so that split stake is > 0 - - let state = StakeState::Stake( - meta, - Stake::just_stake(stake_lamports - rent_exempt_reserve), - ); - - let split_lamport_balances = vec![ - 0, - 1, - expected_rent_exempt_reserve, - expected_rent_exempt_reserve + 1, - ]; - for initial_balance in split_lamport_balances { - let split_stake_account = Account::new_ref_data_with_space( - initial_balance, - &StakeState::Uninitialized, - std::mem::size_of::() + 100, - &id(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - // should return error when initial_balance < expected_rent_exempt_reserve - let split_attempt = - stake_keyed_account.split(split_amount, &split_stake_keyed_account, &signers); - if initial_balance < expected_rent_exempt_reserve { - assert_eq!(split_attempt, Err(InstructionError::InsufficientFunds)); - } else { - assert_eq!(split_attempt, Ok(())); - } - } - } - - #[test] - fn test_split_100_percent_of_source_to_larger_account_edge_case() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake_lamports = rent_exempt_reserve + 1; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - let state = StakeState::Stake( - meta, - Stake::just_stake(stake_lamports - rent_exempt_reserve), - ); - - let expected_rent_exempt_reserve = calculate_split_rent_exempt_reserve( - meta.rent_exempt_reserve, - std::mem::size_of::() as u64, - std::mem::size_of::() as u64 + 100, - ); - assert!(expected_rent_exempt_reserve > stake_lamports); - - let split_lamport_balances = vec![ - 0, - 1, - expected_rent_exempt_reserve, - expected_rent_exempt_reserve + 1, - ]; - for initial_balance in split_lamport_balances { - let split_stake_account = Account::new_ref_data_with_space( - initial_balance, - &StakeState::Uninitialized, - std::mem::size_of::() + 100, - &id(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - // should return error - assert_eq!( - stake_keyed_account.split(stake_lamports, &split_stake_keyed_account, &signers), - Err(InstructionError::InsufficientFunds) - ); - } - } - - #[test] - fn test_split_100_percent_of_source() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake_lamports = rent_exempt_reserve * 3; // Arbitrary amount over rent_exempt_reserve - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - // test splitting both an Initialized stake and a Staked stake - for state in &[ - StakeState::Initialized(meta), - StakeState::Stake( - meta, - Stake::just_stake(stake_lamports - rent_exempt_reserve), - ), - ] { - let split_stake_account = Account::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - // split 100% over to dest - assert_eq!( - stake_keyed_account.split(stake_lamports, &split_stake_keyed_account, &signers), - Ok(()) - ); - - // no lamport leakage - assert_eq!( - stake_keyed_account.account.borrow().lamports - + split_stake_keyed_account.account.borrow().lamports, - stake_lamports - ); - - match state { - StakeState::Initialized(_) => { - assert_eq!(Ok(*state), split_stake_keyed_account.state()); - assert_eq!(Ok(*state), stake_keyed_account.state()); - } - StakeState::Stake(meta, stake) => { - assert_eq!( - Ok(StakeState::Stake( - *meta, - Stake { - delegation: Delegation { - stake: stake_lamports - rent_exempt_reserve, - ..stake.delegation - }, - ..*stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!( - Ok(StakeState::Stake( - *meta, - Stake { - delegation: Delegation { - stake: 0, - ..stake.delegation - }, - ..*stake - } - )), - stake_keyed_account.state() - ); - } - _ => unreachable!(), - } - - // reset - stake_keyed_account.account.borrow_mut().lamports = stake_lamports; - } - } - - #[test] - fn test_split_100_percent_of_source_to_account_with_lamports() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake_lamports = rent_exempt_reserve * 3; // Arbitrary amount over rent_exempt_reserve - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - let state = StakeState::Stake( - meta, - Stake::just_stake(stake_lamports - rent_exempt_reserve), - ); - // Test various account prefunding, including empty, less than rent_exempt_reserve, exactly - // rent_exempt_reserve, and more than rent_exempt_reserve. Technically, the empty case is - // covered in test_split_100_percent_of_source, but included here as well for readability - let split_lamport_balances = vec![0, 1, rent_exempt_reserve, rent_exempt_reserve + 1]; - for initial_balance in split_lamport_balances { - let split_stake_account = Account::new_ref_data_with_space( - initial_balance, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - // split 100% over to dest - assert_eq!( - stake_keyed_account.split(stake_lamports, &split_stake_keyed_account, &signers), - Ok(()) - ); - - // no lamport leakage - assert_eq!( - stake_keyed_account.account.borrow().lamports - + split_stake_keyed_account.account.borrow().lamports, - stake_lamports + initial_balance - ); - - if let StakeState::Stake(meta, stake) = state { - assert_eq!( - Ok(StakeState::Stake( - meta, - Stake { - delegation: Delegation { - stake: stake_lamports - rent_exempt_reserve, - ..stake.delegation - }, - ..stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!( - Ok(StakeState::Stake( - meta, - Stake { - delegation: Delegation { - stake: 0, - ..stake.delegation - }, - ..stake - } - )), - stake_keyed_account.state() - ); - } - } - } - - #[test] - fn test_split_rent_exemptness() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake_lamports = rent_exempt_reserve + 1; - - let split_stake_pubkey = solana_sdk::pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); - - let meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve, - ..Meta::default() - }; - - for state in &[ - StakeState::Initialized(meta), - StakeState::Stake( - meta, - Stake::just_stake(stake_lamports - rent_exempt_reserve), - ), - ] { - // Test that splitting to a larger account with greater rent-exempt requirement fails - // if split amount is too small - let split_stake_account = Account::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::() + 10000, - &id(), - ) - .expect("stake_account"); - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - assert_eq!( - stake_keyed_account.split(stake_lamports, &split_stake_keyed_account, &signers), - Err(InstructionError::InsufficientFunds) - ); - - // Test that splitting from a larger account to a smaller one works. - // Split amount should not matter, assuming other fund criteria are met - let split_stake_account = Account::new_ref_data_with_space( - 0, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let split_stake_keyed_account = - KeyedAccount::new(&split_stake_pubkey, true, &split_stake_account); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &state, - std::mem::size_of::() + 100, - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - assert_eq!( - stake_keyed_account.split(stake_lamports, &split_stake_keyed_account, &signers), - Ok(()) - ); - - assert_eq!( - split_stake_keyed_account.account.borrow().lamports, - stake_lamports - ); - - let expected_rent_exempt_reserve = calculate_split_rent_exempt_reserve( - meta.rent_exempt_reserve, - std::mem::size_of::() as u64 + 100, - std::mem::size_of::() as u64, - ); - let expected_split_meta = Meta { - authorized: Authorized::auto(&stake_pubkey), - rent_exempt_reserve: expected_rent_exempt_reserve, - ..Meta::default() - }; - - match state { - StakeState::Initialized(_) => { - assert_eq!( - Ok(StakeState::Initialized(expected_split_meta)), - split_stake_keyed_account.state() - ); - assert_eq!(Ok(*state), stake_keyed_account.state()); - } - StakeState::Stake(meta, stake) => { - // 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 - rent_exempt_reserve; - - assert_eq!( - Ok(StakeState::Stake( - expected_split_meta, - Stake { - delegation: Delegation { - stake: expected_stake, - ..stake.delegation - }, - ..*stake - } - )), - split_stake_keyed_account.state() - ); - assert_eq!( - split_stake_keyed_account.account.borrow().lamports, - expected_stake - + expected_rent_exempt_reserve - + (rent_exempt_reserve - expected_rent_exempt_reserve) - ); - assert_eq!( - Ok(StakeState::Stake( - *meta, - Stake { - delegation: Delegation { - stake: 0, - ..stake.delegation - }, - ..*stake - } - )), - stake_keyed_account.state() - ); - } - _ => unreachable!(), - } - } - } - - #[test] - fn test_merge() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let source_stake_pubkey = solana_sdk::pubkey::new_rand(); - let authorized_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - - let signers = vec![authorized_pubkey].into_iter().collect(); - - for state in &[ - StakeState::Initialized(Meta::auto(&authorized_pubkey)), - StakeState::Stake( - Meta::auto(&authorized_pubkey), - Stake::just_stake(stake_lamports), - ), - ] { - for source_state in &[ - StakeState::Initialized(Meta::auto(&authorized_pubkey)), - StakeState::Stake( - Meta::auto(&authorized_pubkey), - Stake::just_stake(stake_lamports), - ), - ] { - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - let source_stake_account = Account::new_ref_data_with_space( - stake_lamports, - source_state, - std::mem::size_of::(), - &id(), - ) - .expect("source_stake_account"); - let source_stake_keyed_account = - KeyedAccount::new(&source_stake_pubkey, true, &source_stake_account); - - // Authorized staker signature required... - assert_eq!( - stake_keyed_account.merge( - &source_stake_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &HashSet::new() - ), - Err(InstructionError::MissingRequiredSignature) - ); - - assert_eq!( - stake_keyed_account.merge( - &source_stake_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &signers - ), - Ok(()) - ); - - // check lamports - assert_eq!( - stake_keyed_account.account.borrow().lamports, - stake_lamports * 2 - ); - assert_eq!(source_stake_keyed_account.account.borrow().lamports, 0); - } - } - } - - #[test] - fn test_merge_self_fails() { - let stake_address = Pubkey::new_unique(); - let authority_pubkey = Pubkey::new_unique(); - let signers = HashSet::from_iter(vec![authority_pubkey]); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake_amount = 4242424242; - let stake_lamports = rent_exempt_reserve + stake_amount; - - let meta = Meta { - rent_exempt_reserve, - ..Meta::auto(&authority_pubkey) - }; - let stake = Stake { - delegation: Delegation { - stake: stake_amount, - activation_epoch: 0, - ..Delegation::default() - }, - ..Stake::default() - }; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Stake(meta, stake), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_address, true, &stake_account); - - assert_eq!( - stake_keyed_account.merge( - &stake_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &signers, - ), - Err(InstructionError::InvalidArgument), - ); - } - - #[test] - fn test_merge_incorrect_authorized_staker() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let source_stake_pubkey = solana_sdk::pubkey::new_rand(); - let authorized_pubkey = solana_sdk::pubkey::new_rand(); - let wrong_authorized_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - - let signers = vec![authorized_pubkey].into_iter().collect(); - let wrong_signers = vec![wrong_authorized_pubkey].into_iter().collect(); - - for state in &[ - StakeState::Initialized(Meta::auto(&authorized_pubkey)), - StakeState::Stake( - Meta::auto(&authorized_pubkey), - Stake::just_stake(stake_lamports), - ), - ] { - for source_state in &[ - StakeState::Initialized(Meta::auto(&wrong_authorized_pubkey)), - StakeState::Stake( - Meta::auto(&wrong_authorized_pubkey), - Stake::just_stake(stake_lamports), - ), - ] { - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - let source_stake_account = Account::new_ref_data_with_space( - stake_lamports, - source_state, - std::mem::size_of::(), - &id(), - ) - .expect("source_stake_account"); - let source_stake_keyed_account = - KeyedAccount::new(&source_stake_pubkey, true, &source_stake_account); - - assert_eq!( - stake_keyed_account.merge( - &source_stake_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &wrong_signers, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - assert_eq!( - stake_keyed_account.merge( - &source_stake_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &signers, - ), - Err(StakeError::MergeMismatch.into()) - ); - } - } - } - - #[test] - fn test_merge_invalid_account_data() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let source_stake_pubkey = solana_sdk::pubkey::new_rand(); - let authorized_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let signers = vec![authorized_pubkey].into_iter().collect(); - - for state in &[ - StakeState::Uninitialized, - StakeState::RewardsPool, - StakeState::Initialized(Meta::auto(&authorized_pubkey)), - StakeState::Stake( - Meta::auto(&authorized_pubkey), - Stake::just_stake(stake_lamports), - ), - ] { - for source_state in &[StakeState::Uninitialized, StakeState::RewardsPool] { - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - state, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - let source_stake_account = Account::new_ref_data_with_space( - stake_lamports, - source_state, - std::mem::size_of::(), - &id(), - ) - .expect("source_stake_account"); - let source_stake_keyed_account = - KeyedAccount::new(&source_stake_pubkey, true, &source_stake_account); - - assert_eq!( - stake_keyed_account.merge( - &source_stake_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &signers, - ), - Err(InstructionError::InvalidAccountData) - ); - } - } - } - - #[test] - fn test_merge_fake_stake_source() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let source_stake_pubkey = solana_sdk::pubkey::new_rand(); - let authorized_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - - let signers = vec![authorized_pubkey].into_iter().collect(); - - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Stake( - Meta::auto(&authorized_pubkey), - Stake::just_stake(stake_lamports), - ), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - - let source_stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Stake( - Meta::auto(&authorized_pubkey), - Stake::just_stake(stake_lamports), - ), - std::mem::size_of::(), - &solana_sdk::pubkey::new_rand(), - ) - .expect("source_stake_account"); - let source_stake_keyed_account = - KeyedAccount::new(&source_stake_pubkey, true, &source_stake_account); - - assert_eq!( - stake_keyed_account.merge( - &source_stake_keyed_account, - &Clock::default(), - &StakeHistory::default(), - &signers - ), - Err(InstructionError::IncorrectProgramId) - ); - } - - #[test] - fn test_merge_active_stake() { - let base_lamports = 4242424242; - let stake_address = Pubkey::new_unique(); - let source_address = Pubkey::new_unique(); - let authority_pubkey = Pubkey::new_unique(); - let signers = HashSet::from_iter(vec![authority_pubkey]); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake_amount = base_lamports; - let stake_lamports = rent_exempt_reserve + stake_amount; - let source_amount = base_lamports; - let source_lamports = rent_exempt_reserve + source_amount; - - let meta = Meta { - rent_exempt_reserve, - ..Meta::auto(&authority_pubkey) - }; - let mut stake = Stake { - delegation: Delegation { - stake: stake_amount, - activation_epoch: 0, - ..Delegation::default() - }, - ..Stake::default() - }; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Stake(meta, stake), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_address, true, &stake_account); - - let source_activation_epoch = 2; - let mut source_stake = Stake { - delegation: Delegation { - stake: source_amount, - activation_epoch: source_activation_epoch, - ..stake.delegation - }, - ..stake - }; - let source_account = Account::new_ref_data_with_space( - source_lamports, - &StakeState::Stake(meta, source_stake), - std::mem::size_of::(), - &id(), - ) - .expect("source_account"); - let source_keyed_account = KeyedAccount::new(&source_address, true, &source_account); - - let mut clock = Clock::default(); - let mut stake_history = StakeHistory::default(); - - clock.epoch = 0; - let mut effective = base_lamports; - let mut activating = stake_amount; - let mut deactivating = 0; - stake_history.add( - clock.epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - - fn try_merge( - stake_account: &KeyedAccount, - source_account: &KeyedAccount, - clock: &Clock, - stake_history: &StakeHistory, - signers: &HashSet, - ) -> Result<(), InstructionError> { - let test_stake_account = stake_account.account.clone(); - let test_stake_keyed = - KeyedAccount::new(stake_account.unsigned_key(), true, &test_stake_account); - let test_source_account = source_account.account.clone(); - let test_source_keyed = - KeyedAccount::new(source_account.unsigned_key(), true, &test_source_account); - - test_stake_keyed.merge(&test_source_keyed, clock, stake_history, signers) - } - - // stake activation epoch, source initialized succeeds - assert!(try_merge( - &stake_keyed_account, - &source_keyed_account, - &clock, - &stake_history, - &signers - ) - .is_ok(),); - assert!(try_merge( - &source_keyed_account, - &stake_keyed_account, - &clock, - &stake_history, - &signers - ) - .is_ok(),); - - // both activating fails - loop { - clock.epoch += 1; - if clock.epoch == source_activation_epoch { - activating += source_amount; - } - let delta = - activating.min((effective as f64 * stake.delegation.warmup_cooldown_rate) as u64); - effective += delta; - activating -= delta; - stake_history.add( - clock.epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - if stake_amount == stake.stake(clock.epoch, Some(&stake_history), true) - && source_amount == source_stake.stake(clock.epoch, Some(&stake_history), true) - { - break; - } - assert_eq!( - try_merge( - &stake_keyed_account, - &source_keyed_account, - &clock, - &stake_history, - &signers - ) - .unwrap_err(), - InstructionError::from(StakeError::MergeTransientStake), - ); - assert_eq!( - try_merge( - &source_keyed_account, - &stake_keyed_account, - &clock, - &stake_history, - &signers - ) - .unwrap_err(), - InstructionError::from(StakeError::MergeTransientStake), - ); - } - // Both fully activated works - assert!(try_merge( - &stake_keyed_account, - &source_keyed_account, - &clock, - &stake_history, - &signers - ) - .is_ok(),); - - // deactivate setup for deactivation - let source_deactivation_epoch = clock.epoch + 1; - let stake_deactivation_epoch = clock.epoch + 2; - - // active/deactivating and deactivating/inactive mismatches fail - loop { - clock.epoch += 1; - let delta = - deactivating.min((effective as f64 * stake.delegation.warmup_cooldown_rate) as u64); - effective -= delta; - deactivating -= delta; - if clock.epoch == stake_deactivation_epoch { - deactivating += stake_amount; - stake = Stake { - delegation: Delegation { - deactivation_epoch: stake_deactivation_epoch, - ..stake.delegation - }, - ..stake - }; - stake_keyed_account - .set_state(&StakeState::Stake(meta, stake)) - .unwrap(); - } - if clock.epoch == source_deactivation_epoch { - deactivating += source_amount; - source_stake = Stake { - delegation: Delegation { - deactivation_epoch: source_deactivation_epoch, - ..source_stake.delegation - }, - ..source_stake - }; - source_keyed_account - .set_state(&StakeState::Stake(meta, source_stake)) - .unwrap(); - } - stake_history.add( - clock.epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - if 0 == stake.stake(clock.epoch, Some(&stake_history), true) - && 0 == source_stake.stake(clock.epoch, Some(&stake_history), true) - { - break; - } - assert_eq!( - try_merge( - &stake_keyed_account, - &source_keyed_account, - &clock, - &stake_history, - &signers - ) - .unwrap_err(), - InstructionError::from(StakeError::MergeTransientStake), - ); - assert_eq!( - try_merge( - &source_keyed_account, - &stake_keyed_account, - &clock, - &stake_history, - &signers - ) - .unwrap_err(), - InstructionError::from(StakeError::MergeTransientStake), - ); - } - - // Both fully deactivated works - assert!(try_merge( - &stake_keyed_account, - &source_keyed_account, - &clock, - &stake_history, - &signers - ) - .is_ok(),); - } - - #[test] - fn test_lockup_is_expired() { - let custodian = solana_sdk::pubkey::new_rand(); - let lockup = Lockup { - epoch: 1, - unix_timestamp: 1, - custodian, - }; - // neither time - assert_eq!( - lockup.is_in_force( - &Clock { - epoch: 0, - unix_timestamp: 0, - ..Clock::default() - }, - None - ), - true - ); - // not timestamp - assert_eq!( - lockup.is_in_force( - &Clock { - epoch: 2, - unix_timestamp: 0, - ..Clock::default() - }, - None - ), - true - ); - // not epoch - assert_eq!( - lockup.is_in_force( - &Clock { - epoch: 0, - unix_timestamp: 2, - ..Clock::default() - }, - None - ), - true - ); - // both, no custodian - assert_eq!( - lockup.is_in_force( - &Clock { - epoch: 1, - unix_timestamp: 1, - ..Clock::default() - }, - None - ), - false - ); - // neither, but custodian - assert_eq!( - lockup.is_in_force( - &Clock { - epoch: 0, - unix_timestamp: 0, - ..Clock::default() - }, - Some(&custodian), - ), - false, - ); - } - - #[test] - #[ignore] - #[should_panic] - fn test_dbg_stake_minimum_balance() { - let minimum_balance = Rent::default().minimum_balance(std::mem::size_of::()); - panic!( - "stake minimum_balance: {} lamports, {} SOL", - minimum_balance, - minimum_balance as f64 / solana_sdk::native_token::LAMPORTS_PER_SOL as f64 - ); - } - - #[test] - fn test_authorize_delegated_stake() { - let stake_pubkey = solana_sdk::pubkey::new_rand(); - let stake_lamports = 42; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(Meta::auto(&stake_pubkey)), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - - let clock = Clock::default(); - - let vote_pubkey = solana_sdk::pubkey::new_rand(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let signers = vec![stake_pubkey].into_iter().collect(); - stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &signers, - ) - .unwrap(); - - // deactivate, so we can re-delegate - stake_keyed_account.deactivate(&clock, &signers).unwrap(); - - let new_staker_pubkey = solana_sdk::pubkey::new_rand(); - assert_eq!( - stake_keyed_account.authorize(&signers, &new_staker_pubkey, StakeAuthorize::Staker), - Ok(()) - ); - let authorized = - StakeState::authorized_from(&stake_keyed_account.try_account_ref().unwrap()).unwrap(); - assert_eq!(authorized.staker, new_staker_pubkey); - - let other_pubkey = solana_sdk::pubkey::new_rand(); - let other_signers = vec![other_pubkey].into_iter().collect(); - - // Use unsigned stake_keyed_account to test other signers - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account); - - let new_voter_pubkey = solana_sdk::pubkey::new_rand(); - let vote_state = VoteState::default(); - let new_vote_account = RefCell::new(vote_state::create_account( - &new_voter_pubkey, - &solana_sdk::pubkey::new_rand(), - 0, - 100, - )); - let new_vote_keyed_account = KeyedAccount::new(&new_voter_pubkey, false, &new_vote_account); - new_vote_keyed_account.set_state(&vote_state).unwrap(); - - // Random other account should fail - assert_eq!( - stake_keyed_account.delegate( - &new_vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &other_signers, - ), - Err(InstructionError::MissingRequiredSignature) - ); - - let new_signers = vec![new_staker_pubkey].into_iter().collect(); - // Authorized staker should succeed - assert_eq!( - stake_keyed_account.delegate( - &new_vote_keyed_account, - &clock, - &StakeHistory::default(), - &Config::default(), - &new_signers - ), - Ok(()) - ); - let stake = - StakeState::stake_from(&stake_keyed_account.try_account_ref().unwrap()).unwrap(); - assert_eq!(stake.delegation.voter_pubkey, new_voter_pubkey); - - // Test another staking action - assert_eq!(stake_keyed_account.deactivate(&clock, &new_signers), Ok(())); - } - - #[test] - fn test_redelegate_consider_balance_changes() { - let initial_lamports = 4242424242; - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let withdrawer_pubkey = Pubkey::new_unique(); - let stake_lamports = rent_exempt_reserve + initial_lamports; - - let meta = Meta { - rent_exempt_reserve, - ..Meta::auto(&withdrawer_pubkey) - }; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Initialized(meta), - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account); - - let vote_pubkey = Pubkey::new_unique(); - let vote_account = RefCell::new(vote_state::create_account( - &vote_pubkey, - &Pubkey::new_unique(), - 0, - 100, - )); - let vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &vote_account); - - let signers = HashSet::from_iter(vec![withdrawer_pubkey]); - let config = Config::default(); - let stake_history = StakeHistory::default(); - let mut clock = Clock::default(); - stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &stake_history, - &config, - &signers, - ) - .unwrap(); - - clock.epoch += 1; - stake_keyed_account.deactivate(&clock, &signers).unwrap(); - - clock.epoch += 1; - let to = Pubkey::new_unique(); - let to_account = Account::new_ref(1, 0, &system_program::id()); - let to_keyed_account = KeyedAccount::new(&to, false, &to_account); - let withdraw_lamports = initial_lamports / 2; - stake_keyed_account - .withdraw( - withdraw_lamports, - &to_keyed_account, - &clock, - &stake_history, - &stake_keyed_account, - None, - ) - .unwrap(); - let expected_balance = rent_exempt_reserve + initial_lamports - withdraw_lamports; - assert_eq!(stake_keyed_account.lamports().unwrap(), expected_balance); - - clock.epoch += 1; - stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &stake_history, - &config, - &signers, - ) - .unwrap(); - let stake = StakeState::stake_from(&stake_account.borrow()).unwrap(); - assert_eq!( - stake.delegation.stake, - stake_keyed_account.lamports().unwrap() - rent_exempt_reserve, - ); - - clock.epoch += 1; - stake_keyed_account.deactivate(&clock, &signers).unwrap(); - - // Out of band deposit - stake_keyed_account.try_account_ref_mut().unwrap().lamports += withdraw_lamports; - - clock.epoch += 1; - stake_keyed_account - .delegate( - &vote_keyed_account, - &clock, - &stake_history, - &config, - &signers, - ) - .unwrap(); - let stake = StakeState::stake_from(&stake_account.borrow()).unwrap(); - assert_eq!( - stake.delegation.stake, - stake_keyed_account.lamports().unwrap() - rent_exempt_reserve, - ); - } - - #[test] - fn test_meta_rewrite_rent_exempt_reserve() { - let right_data_len = std::mem::size_of::() as u64; - let rent = Rent::default(); - let expected_rent_exempt_reserve = rent.minimum_balance(right_data_len as usize); - - let test_cases = [ - ( - right_data_len + 100, - Some(( - rent.minimum_balance(right_data_len as usize + 100), - expected_rent_exempt_reserve, - )), - ), // large data_len, too small rent exempt - (right_data_len, None), // correct - ( - right_data_len - 100, - Some(( - rent.minimum_balance(right_data_len as usize - 100), - expected_rent_exempt_reserve, - )), - ), // small data_len, too large rent exempt - ]; - for (data_len, expected_rewrite) in &test_cases { - let rent_exempt_reserve = rent.minimum_balance(*data_len as usize); - let mut meta = Meta { - rent_exempt_reserve, - ..Meta::default() - }; - let actual_rewrite = meta.rewrite_rent_exempt_reserve(&rent, right_data_len as usize); - assert_eq!(actual_rewrite, *expected_rewrite); - assert_eq!(meta.rent_exempt_reserve, expected_rent_exempt_reserve); - } - } - - #[test] - fn test_stake_rewrite_stake() { - let right_data_len = std::mem::size_of::() as u64; - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(right_data_len as usize); - let expected_stake = 1000; - let account_balance = rent_exempt_reserve + expected_stake; - - let test_cases = [ - (9999, Some((9999, expected_stake))), // large stake - (1000, None), // correct - (42, Some((42, expected_stake))), // small stake - ]; - for (staked_amount, expected_rewrite) in &test_cases { - let mut delegation = Delegation { - stake: *staked_amount, - ..Delegation::default() - }; - let actual_rewrite = delegation.rewrite_stake(account_balance, rent_exempt_reserve); - assert_eq!(actual_rewrite, *expected_rewrite); - assert_eq!(delegation.stake, expected_stake); - } - } - - enum ExpectedRewriteResult { - NotRewritten, - Rewritten, - } - - #[test] - fn test_rewrite_stakes_initialized() { - let right_data_len = std::mem::size_of::(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(right_data_len as usize); - let expected_stake = 1000; - let account_balance = rent_exempt_reserve + expected_stake; - - let test_cases = [ - (1, ExpectedRewriteResult::Rewritten), - (0, ExpectedRewriteResult::NotRewritten), - ]; - for (offset, expected_rewrite) in &test_cases { - let meta = Meta { - rent_exempt_reserve: rent_exempt_reserve + offset, - ..Meta::default() - }; - let mut account = Account::new(account_balance, right_data_len, &id()); - account.set_state(&StakeState::Initialized(meta)).unwrap(); - let result = rewrite_stakes(&mut account, &rent); - match expected_rewrite { - ExpectedRewriteResult::NotRewritten => assert!(result.is_err()), - ExpectedRewriteResult::Rewritten => assert!(result.is_ok()), - } - } - } - - #[test] - fn test_rewrite_stakes_stake() { - let right_data_len = std::mem::size_of::(); - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(right_data_len as usize); - let expected_stake = 1000; - let account_balance = rent_exempt_reserve + expected_stake; - - let test_cases = [ - (1, 9999, ExpectedRewriteResult::Rewritten), // bad meta, bad stake - (1, 1000, ExpectedRewriteResult::Rewritten), // bad meta, good stake - (0, 9999, ExpectedRewriteResult::Rewritten), // good meta, bad stake - (0, 1000, ExpectedRewriteResult::NotRewritten), // good meta, good stake - ]; - for (offset, staked_amount, expected_rewrite) in &test_cases { - let meta = Meta { - rent_exempt_reserve: rent_exempt_reserve + offset, - ..Meta::default() - }; - let stake = Stake { - delegation: (Delegation { - stake: *staked_amount, - ..Delegation::default() - }), - ..Stake::default() - }; - let mut account = Account::new(account_balance, right_data_len, &id()); - account.set_state(&StakeState::Stake(meta, stake)).unwrap(); - let result = rewrite_stakes(&mut account, &rent); - match expected_rewrite { - ExpectedRewriteResult::NotRewritten => assert!(result.is_err()), - ExpectedRewriteResult::Rewritten => assert!(result.is_ok()), - } - } - } - - #[test] - fn test_calculate_lamports_per_byte_year() { - let rent = Rent::default(); - let data_len = 200u64; - let rent_exempt_reserve = rent.minimum_balance(data_len as usize); - assert_eq!( - calculate_split_rent_exempt_reserve(rent_exempt_reserve, data_len, data_len), - rent_exempt_reserve - ); - - let larger_data = 4008u64; - let larger_rent_exempt_reserve = rent.minimum_balance(larger_data as usize); - assert_eq!( - calculate_split_rent_exempt_reserve(rent_exempt_reserve, data_len, larger_data), - larger_rent_exempt_reserve - ); - assert_eq!( - calculate_split_rent_exempt_reserve(larger_rent_exempt_reserve, larger_data, data_len), - rent_exempt_reserve - ); - - let even_larger_data = solana_sdk::system_instruction::MAX_PERMITTED_DATA_LENGTH; - let even_larger_rent_exempt_reserve = rent.minimum_balance(even_larger_data as usize); - assert_eq!( - calculate_split_rent_exempt_reserve(rent_exempt_reserve, data_len, even_larger_data), - even_larger_rent_exempt_reserve - ); - assert_eq!( - calculate_split_rent_exempt_reserve( - even_larger_rent_exempt_reserve, - even_larger_data, - data_len - ), - rent_exempt_reserve - ); - } - - #[test] - fn test_things_can_merge() { - let good_stake = Stake { - credits_observed: 4242, - delegation: Delegation { - voter_pubkey: Pubkey::new_unique(), - stake: 424242424242, - activation_epoch: 42, - ..Delegation::default() - }, - }; - - let identical = good_stake; - assert!(MergeKind::active_stakes_can_merge(&good_stake, &identical).is_ok()); - - let bad_credits_observed = Stake { - credits_observed: good_stake.credits_observed + 1, - ..good_stake - }; - assert!(MergeKind::active_stakes_can_merge(&good_stake, &bad_credits_observed).is_err()); - - let good_delegation = good_stake.delegation; - let different_stake_ok = Delegation { - stake: good_delegation.stake + 1, - ..good_delegation - }; - assert!( - MergeKind::active_delegations_can_merge(&good_delegation, &different_stake_ok).is_ok() - ); - - let different_activation_epoch_ok = Delegation { - activation_epoch: good_delegation.activation_epoch + 1, - ..good_delegation - }; - assert!(MergeKind::active_delegations_can_merge( - &good_delegation, - &different_activation_epoch_ok - ) - .is_ok()); - - let bad_voter = Delegation { - voter_pubkey: Pubkey::new_unique(), - ..good_delegation - }; - assert!(MergeKind::active_delegations_can_merge(&good_delegation, &bad_voter).is_err()); - - let bad_warmup_cooldown_rate = Delegation { - warmup_cooldown_rate: good_delegation.warmup_cooldown_rate + f64::EPSILON, - ..good_delegation - }; - assert!(MergeKind::active_delegations_can_merge( - &good_delegation, - &bad_warmup_cooldown_rate - ) - .is_err()); - assert!(MergeKind::active_delegations_can_merge( - &bad_warmup_cooldown_rate, - &good_delegation - ) - .is_err()); - - let bad_deactivation_epoch = Delegation { - deactivation_epoch: 43, - ..good_delegation - }; - assert!( - MergeKind::active_delegations_can_merge(&good_delegation, &bad_deactivation_epoch) - .is_err() - ); - assert!( - MergeKind::active_delegations_can_merge(&bad_deactivation_epoch, &good_delegation) - .is_err() - ); - - // Identical Metas can merge - assert!(MergeKind::metas_can_merge(&Meta::default(), &Meta::default()).is_ok()); - - let mismatched_rent_exempt_reserve_ok = Meta { - rent_exempt_reserve: 42, - ..Meta::default() - }; - assert_ne!( - mismatched_rent_exempt_reserve_ok.rent_exempt_reserve, - Meta::default().rent_exempt_reserve - ); - assert!( - MergeKind::metas_can_merge(&Meta::default(), &mismatched_rent_exempt_reserve_ok) - .is_ok() - ); - assert!( - MergeKind::metas_can_merge(&mismatched_rent_exempt_reserve_ok, &Meta::default()) - .is_ok() - ); - - let mismatched_authorized_fails = Meta { - authorized: Authorized { - staker: Pubkey::new_unique(), - withdrawer: Pubkey::new_unique(), - }, - ..Meta::default() - }; - assert_ne!( - mismatched_authorized_fails.authorized, - Meta::default().authorized - ); - assert!( - MergeKind::metas_can_merge(&Meta::default(), &mismatched_authorized_fails).is_err() - ); - assert!( - MergeKind::metas_can_merge(&mismatched_authorized_fails, &Meta::default()).is_err() - ); - - let mismatched_lockup_fails = Meta { - lockup: Lockup { - unix_timestamp: 424242424, - epoch: 42, - custodian: Pubkey::new_unique(), - }, - ..Meta::default() - }; - assert_ne!(mismatched_lockup_fails.lockup, Meta::default().lockup); - assert!(MergeKind::metas_can_merge(&Meta::default(), &mismatched_lockup_fails).is_err()); - assert!(MergeKind::metas_can_merge(&mismatched_lockup_fails, &Meta::default()).is_err()); - } - - #[test] - fn test_merge_kind_get_if_mergeable() { - let authority_pubkey = Pubkey::new_unique(); - let initial_lamports = 4242424242; - let rent = Rent::default(); - let rent_exempt_reserve = rent.minimum_balance(std::mem::size_of::()); - let stake_lamports = rent_exempt_reserve + initial_lamports; - - let meta = Meta { - rent_exempt_reserve, - ..Meta::auto(&authority_pubkey) - }; - let stake_account = Account::new_ref_data_with_space( - stake_lamports, - &StakeState::Uninitialized, - std::mem::size_of::(), - &id(), - ) - .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&authority_pubkey, true, &stake_account); - - let mut clock = Clock::default(); - let mut stake_history = StakeHistory::default(); - - // Uninitialized state fails - assert_eq!( - MergeKind::get_if_mergeable(&stake_keyed_account, &clock, &stake_history).unwrap_err(), - InstructionError::InvalidAccountData - ); - - // RewardsPool state fails - stake_keyed_account - .set_state(&StakeState::RewardsPool) - .unwrap(); - assert_eq!( - MergeKind::get_if_mergeable(&stake_keyed_account, &clock, &stake_history).unwrap_err(), - InstructionError::InvalidAccountData - ); - - // Initialized state succeeds - stake_keyed_account - .set_state(&StakeState::Initialized(meta)) - .unwrap(); - assert_eq!( - MergeKind::get_if_mergeable(&stake_keyed_account, &clock, &stake_history).unwrap(), - MergeKind::Inactive(meta, stake_lamports) - ); - - clock.epoch = 0; - let mut effective = 2 * initial_lamports; - let mut activating = 0; - let mut deactivating = 0; - stake_history.add( - clock.epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - - clock.epoch += 1; - activating = initial_lamports; - stake_history.add( - clock.epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - - let stake = Stake { - delegation: Delegation { - stake: initial_lamports, - activation_epoch: 1, - deactivation_epoch: 5, - ..Delegation::default() - }, - ..Stake::default() - }; - stake_keyed_account - .set_state(&StakeState::Stake(meta, stake)) - .unwrap(); - // activation_epoch succeeds - assert_eq!( - MergeKind::get_if_mergeable(&stake_keyed_account, &clock, &stake_history).unwrap(), - MergeKind::ActivationEpoch(meta, stake), - ); - - // all paritially activated, transient epochs fail - loop { - clock.epoch += 1; - let delta = - activating.min((effective as f64 * stake.delegation.warmup_cooldown_rate) as u64); - effective += delta; - activating -= delta; - stake_history.add( - clock.epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - if activating == 0 { - break; - } - assert_eq!( - MergeKind::get_if_mergeable(&stake_keyed_account, &clock, &stake_history) - .unwrap_err(), - InstructionError::from(StakeError::MergeTransientStake), - ); - } - - // all epochs for which we're fully active succeed - while clock.epoch < stake.delegation.deactivation_epoch - 1 { - clock.epoch += 1; - stake_history.add( - clock.epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - assert_eq!( - MergeKind::get_if_mergeable(&stake_keyed_account, &clock, &stake_history).unwrap(), - MergeKind::FullyActive(meta, stake), - ); - } - - clock.epoch += 1; - deactivating = stake.delegation.stake; - stake_history.add( - clock.epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - // deactivation epoch fails, fully transient/deactivating - assert_eq!( - MergeKind::get_if_mergeable(&stake_keyed_account, &clock, &stake_history).unwrap_err(), - InstructionError::from(StakeError::MergeTransientStake), - ); - - // all transient, deactivating epochs fail - loop { - clock.epoch += 1; - let delta = - deactivating.min((effective as f64 * stake.delegation.warmup_cooldown_rate) as u64); - effective -= delta; - deactivating -= delta; - stake_history.add( - clock.epoch, - StakeHistoryEntry { - effective, - activating, - deactivating, - }, - ); - if deactivating == 0 { - break; - } - assert_eq!( - MergeKind::get_if_mergeable(&stake_keyed_account, &clock, &stake_history) - .unwrap_err(), - InstructionError::from(StakeError::MergeTransientStake), - ); - } - - // first fully-deactivated epoch succeeds - assert_eq!( - MergeKind::get_if_mergeable(&stake_keyed_account, &clock, &stake_history).unwrap(), - MergeKind::Inactive(meta, stake_lamports), - ); - } - - #[test] - fn test_merge_kind_merge() { - let lamports = 424242; - let meta = Meta { - rent_exempt_reserve: 42, - ..Meta::default() - }; - let stake = Stake { - delegation: Delegation { - stake: 4242, - ..Delegation::default() - }, - ..Stake::default() - }; - let inactive = MergeKind::Inactive(Meta::default(), lamports); - let activation_epoch = MergeKind::ActivationEpoch(meta, stake); - let fully_active = MergeKind::FullyActive(meta, stake); - - assert_eq!(inactive.clone().merge(inactive.clone()).unwrap(), None); - assert_eq!( - inactive.clone().merge(activation_epoch.clone()).unwrap(), - None - ); - assert!(inactive.clone().merge(fully_active.clone()).is_err()); - assert!(activation_epoch - .clone() - .merge(fully_active.clone()) - .is_err()); - assert!(fully_active.clone().merge(inactive.clone()).is_err()); - assert!(fully_active - .clone() - .merge(activation_epoch.clone()) - .is_err()); - - let new_state = activation_epoch.clone().merge(inactive).unwrap().unwrap(); - let delegation = new_state.delegation().unwrap(); - assert_eq!(delegation.stake, stake.delegation.stake + lamports); - - let new_state = activation_epoch - .clone() - .merge(activation_epoch) - .unwrap() - .unwrap(); - let delegation = new_state.delegation().unwrap(); - assert_eq!( - delegation.stake, - 2 * stake.delegation.stake + meta.rent_exempt_reserve - ); - - let new_state = fully_active.clone().merge(fully_active).unwrap().unwrap(); - let delegation = new_state.delegation().unwrap(); - assert_eq!(delegation.stake, 2 * stake.delegation.stake); - } -} diff --git a/programs/stake/src/lib.rs b/programs/stake/src/lib.rs index a08f5beb21..57c4616bc4 100644 --- a/programs/stake/src/lib.rs +++ b/programs/stake/src/lib.rs @@ -2,8 +2,6 @@ use solana_sdk::genesis_config::GenesisConfig; pub mod config; -pub mod legacy_stake_processor; -pub mod legacy_stake_state; pub mod stake_instruction; pub mod stake_state; diff --git a/runtime/src/builtins.rs b/runtime/src/builtins.rs index c1899df280..03a9dfc7ec 100644 --- a/runtime/src/builtins.rs +++ b/runtime/src/builtins.rs @@ -60,14 +60,10 @@ fn genesis_builtins() -> Vec { solana_vote_program::id(), with_program_logging!(solana_vote_program::vote_instruction::process_instruction), ), - // Remove legacy_stake_processor and move stake_instruction::process_instruction back to - // genesis_builtins around the v1.6 timeframe Builtin::new( "stake_program", solana_stake_program::id(), - with_program_logging!( - solana_stake_program::legacy_stake_processor::process_instruction - ), + with_program_logging!(solana_stake_program::stake_instruction::process_instruction), ), Builtin::new( "config_program", @@ -92,26 +88,15 @@ pub enum ActivationType { /// normal child Bank creation. /// https://github.com/solana-labs/solana/blob/84b139cc94b5be7c9e0c18c2ad91743231b85a0d/runtime/src/bank.rs#L1723 fn feature_builtins() -> Vec<(Builtin, Pubkey, ActivationType)> { - vec![ - ( - Builtin::new( - "secp256k1_program", - solana_sdk::secp256k1_program::id(), - solana_secp256k1_program::process_instruction, - ), - feature_set::secp256k1_program_enabled::id(), - ActivationType::NewProgram, + vec![( + Builtin::new( + "secp256k1_program", + solana_sdk::secp256k1_program::id(), + solana_secp256k1_program::process_instruction, ), - ( - Builtin::new( - "stake_program_v3", - solana_stake_program::id(), - solana_stake_program::stake_instruction::process_instruction, - ), - feature_set::stake_program_v3::id(), - ActivationType::NewVersion, - ), - ] + feature_set::secp256k1_program_enabled::id(), + ActivationType::NewProgram, + )] } pub(crate) fn get() -> Builtins {