diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 8232df0b8d..3786fd224a 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1073,7 +1073,7 @@ mod test { assert_eq!( bank_forks.read().unwrap().get_fork_confidence(0).unwrap(), - &Confidence::new(0, 1, 2) + &Confidence::new(0, 3, 2) ); assert!(bank_forks.read().unwrap().get_fork_confidence(1).is_none()); @@ -1112,15 +1112,15 @@ mod test { assert_eq!( bank_forks.read().unwrap().get_fork_confidence(0).unwrap(), - &Confidence::new_with_stake_weighted(1, 1, 14, 20) + &Confidence::new_with_stake_weighted(3, 3, 14, 60) ); assert_eq!( bank_forks.read().unwrap().get_fork_confidence(1).unwrap(), - &Confidence::new_with_stake_weighted(1, 1, 6, 6) + &Confidence::new_with_stake_weighted(3, 3, 6, 18) ); assert_eq!( bank_forks.read().unwrap().get_fork_confidence(2).unwrap(), - &Confidence::new_with_stake_weighted(0, 1, 2, 0) + &Confidence::new_with_stake_weighted(0, 3, 2, 0) ); } } diff --git a/core/src/staking_utils.rs b/core/src/staking_utils.rs index 55791c41ab..0c6e06c52b 100644 --- a/core/src/staking_utils.rs +++ b/core/src/staking_utils.rs @@ -119,6 +119,7 @@ pub(crate) mod tests { use solana_sdk::instruction::Instruction; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, KeypairUtil}; + use solana_sdk::sysvar::stake_history::{self, StakeHistory}; use solana_sdk::transaction::Transaction; use solana_stake_api::stake_instruction; use solana_stake_api::stake_state::Stake; @@ -145,11 +146,12 @@ pub(crate) mod tests { let leader_stake = Stake { stake: BOOTSTRAP_LEADER_LAMPORTS, + activated: std::u64::MAX, // exempt from warmup ..Stake::default() }; // First epoch has the bootstrap leader - expected.insert(voting_keypair.pubkey(), leader_stake.stake(0)); + expected.insert(voting_keypair.pubkey(), leader_stake.stake(0, None)); // henceforth, verify that we have snapshots of stake at epoch 0 let expected = Some(expected); @@ -214,6 +216,7 @@ pub(crate) mod tests { let stake = BOOTSTRAP_LEADER_LAMPORTS * 100; let leader_stake = Stake { stake: BOOTSTRAP_LEADER_LAMPORTS, + activated: std::u64::MAX, // mark as bootstrap ..Stake::default() }; @@ -251,26 +254,35 @@ pub(crate) mod tests { ..Stake::default() }; - let epoch = bank.get_stakers_epoch(bank.slot()); + let first_stakers_epoch = bank.get_stakers_epoch(bank.slot()); // find the first slot in the next staker's epoch - let mut slot = 1; - while bank.get_stakers_epoch(slot) <= epoch { + let mut slot = bank.slot(); + loop { slot += 1; + if bank.get_stakers_epoch(slot) != first_stakers_epoch { + break; + } } let bank = new_from_parent(&Arc::new(bank), slot); - let epoch = bank.get_stakers_epoch(slot); + let next_stakers_epoch = bank.get_stakers_epoch(slot); - let result: Vec<_> = epoch_stakes_and_lockouts(&bank, 0); - assert_eq!(result, vec![(leader_stake.stake(0), None)]); + let result: Vec<_> = epoch_stakes_and_lockouts(&bank, first_stakers_epoch); + assert_eq!( + result, + vec![(leader_stake.stake(first_stakers_epoch, None), None)] + ); // epoch stakes and lockouts are saved off for the future epoch, should // match current bank state - let mut result: Vec<_> = epoch_stakes_and_lockouts(&bank, epoch); + let mut result: Vec<_> = epoch_stakes_and_lockouts(&bank, next_stakers_epoch); result.sort(); + let stake_history = + StakeHistory::from(&bank.get_account(&stake_history::id()).unwrap()).unwrap(); let mut expected = vec![ - (leader_stake.stake(bank.epoch()), None), - (other_stake.stake(bank.epoch()), None), + (leader_stake.stake(bank.epoch(), Some(&stake_history)), None), + (other_stake.stake(bank.epoch(), Some(&stake_history)), None), ]; + expected.sort(); assert_eq!(result, expected); } diff --git a/programs/stake_api/src/stake_instruction.rs b/programs/stake_api/src/stake_instruction.rs index 4d79627312..4f392e4528 100644 --- a/programs/stake_api/src/stake_instruction.rs +++ b/programs/stake_api/src/stake_instruction.rs @@ -3,11 +3,12 @@ use crate::stake_state::{StakeAccount, StakeState}; use bincode::deserialize; use log::*; use serde_derive::{Deserialize, Serialize}; -use solana_sdk::account::KeyedAccount; -use solana_sdk::instruction::{AccountMeta, Instruction, InstructionError}; -use solana_sdk::pubkey::Pubkey; -use solana_sdk::system_instruction; -use solana_sdk::sysvar; +use solana_sdk::{ + account::KeyedAccount, + instruction::{AccountMeta, Instruction, InstructionError}, + pubkey::Pubkey, + system_instruction, sysvar, +}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum StakeInstruction { @@ -30,6 +31,7 @@ pub enum StakeInstruction { /// 1 - VoteAccount to which the Stake is delegated, /// 2 - RewardsPool Stake Account from which to redeem credits /// 3 - Rewards sysvar Account that carries points values + /// 4 - StakeHistory sysvar that carries stake warmup/cooldown history RedeemVoteCredits, /// Withdraw unstaked lamports from the stake account @@ -38,6 +40,7 @@ pub enum StakeInstruction { /// 0 - Delegate StakeAccount /// 1 - System account to which the lamports will be transferred, /// 2 - Syscall Account that carries epoch + /// 3 - StakeHistory sysvar that carries stake warmup/cooldown history /// /// The u64 is the portion of the Stake account balance to be withdrawn, /// must be <= StakeAccount.lamports - staked lamports @@ -83,6 +86,7 @@ pub fn redeem_vote_credits(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey) -> Instr AccountMeta::new_credit_only(*vote_pubkey, false), AccountMeta::new(crate::rewards_pools::random_id(), false), AccountMeta::new_credit_only(sysvar::rewards::id(), false), + AccountMeta::new_credit_only(sysvar::stake_history::id(), false), ]; Instruction::new(id(), &StakeInstruction::RedeemVoteCredits, account_metas) } @@ -101,6 +105,7 @@ pub fn withdraw(stake_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64) -> Ins AccountMeta::new(*stake_pubkey, true), AccountMeta::new_credit_only(*to_pubkey, false), AccountMeta::new_credit_only(sysvar::clock::id(), false), + AccountMeta::new_credit_only(sysvar::stake_history::id(), false), ]; Instruction::new(id(), &StakeInstruction::Withdraw(lamports), account_metas) } @@ -142,7 +147,7 @@ pub fn process_instruction( me.delegate_stake(vote, stake, &sysvar::clock::from_keyed_account(&rest[1])?) } StakeInstruction::RedeemVoteCredits => { - if rest.len() != 3 { + if rest.len() != 4 { Err(InstructionError::InvalidInstructionData)?; } let (vote, rest) = rest.split_at_mut(1); @@ -154,10 +159,11 @@ pub fn process_instruction( vote, rewards_pool, &sysvar::rewards::from_keyed_account(&rest[0])?, + &sysvar::stake_history::from_keyed_account(&rest[1])?, ) } StakeInstruction::Withdraw(lamports) => { - if rest.len() != 2 { + if rest.len() != 3 { Err(InstructionError::InvalidInstructionData)?; } let (to, sysvar) = &mut rest.split_at_mut(1); @@ -167,6 +173,7 @@ pub fn process_instruction( lamports, &mut to, &sysvar::clock::from_keyed_account(&sysvar[0])?, + &sysvar::stake_history::from_keyed_account(&sysvar[1])?, ) } StakeInstruction::Deactivate => { @@ -186,7 +193,7 @@ pub fn process_instruction( mod tests { use super::*; use bincode::serialize; - use solana_sdk::account::Account; + use solana_sdk::{account::Account, sysvar::stake_history::StakeHistory}; fn process_instruction(instruction: &Instruction) -> Result<(), InstructionError> { let mut accounts: Vec<_> = instruction @@ -197,6 +204,8 @@ mod tests { sysvar::clock::create_account(1, 0, 0, 0, 0) } else if sysvar::rewards::check_id(&meta.pubkey) { sysvar::rewards::create_account(1, 0.0, 0.0) + } else if sysvar::stake_history::check_id(&meta.pubkey) { + sysvar::stake_history::create_account(1, &StakeHistory::default()) } else { Account::default() } @@ -217,7 +226,7 @@ mod tests { #[test] fn test_stake_process_instruction() { assert_eq!( - process_instruction(&redeem_vote_credits(&Pubkey::default(), &Pubkey::default(),)), + process_instruction(&redeem_vote_credits(&Pubkey::default(), &Pubkey::default())), Err(InstructionError::InvalidAccountData), ); assert_eq!( @@ -309,6 +318,11 @@ mod tests { false, &mut sysvar::rewards::create_account(1, 0.0, 0.0) ), + KeyedAccount::new( + &sysvar::stake_history::id(), + false, + &mut sysvar::stake_history::create_account(1, &StakeHistory::default()) + ), ], &serialize(&StakeInstruction::RedeemVoteCredits).unwrap(), ), @@ -327,6 +341,11 @@ mod tests { false, &mut sysvar::rewards::create_account(1, 0.0, 0.0) ), + KeyedAccount::new( + &sysvar::stake_history::id(), + false, + &mut sysvar::stake_history::create_account(1, &StakeHistory::default()) + ), ], &serialize(&StakeInstruction::Withdraw(42)).unwrap(), ), @@ -344,6 +363,11 @@ mod tests { false, &mut sysvar::rewards::create_account(1, 0.0, 0.0) ), + KeyedAccount::new( + &sysvar::stake_history::id(), + false, + &mut sysvar::stake_history::create_account(1, &StakeHistory::default()) + ), ], &serialize(&StakeInstruction::Withdraw(42)).unwrap(), ), diff --git a/programs/stake_api/src/stake_state.rs b/programs/stake_api/src/stake_state.rs index 0fe49abb0f..0ad72af7d2 100644 --- a/programs/stake_api/src/stake_state.rs +++ b/programs/stake_api/src/stake_state.rs @@ -5,14 +5,18 @@ use crate::id; use serde_derive::{Deserialize, Serialize}; -use solana_sdk::account::{Account, KeyedAccount}; -use solana_sdk::account_utils::State; -use solana_sdk::instruction::InstructionError; -use solana_sdk::pubkey::Pubkey; -use solana_sdk::sysvar; -use solana_sdk::timing::Epoch; +use solana_sdk::{ + account::{Account, KeyedAccount}, + account_utils::State, + instruction::InstructionError, + pubkey::Pubkey, + sysvar::{ + self, + stake_history::{StakeHistory, StakeHistoryEntry}, + }, + timing::Epoch, +}; use solana_vote_api::vote_state::VoteState; -use std::cmp; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub enum StakeState { @@ -50,14 +54,15 @@ pub struct Stake { pub voter_pubkey: Pubkey, pub credits_observed: u64, pub stake: u64, // stake amount activated - pub activated: Epoch, // epoch the stake was activated + pub activated: Epoch, // epoch the stake was activated, std::Epoch::MAX if is a bootstrap stake pub deactivated: Epoch, // epoch the stake was deactivated, std::Epoch::MAX if not deactivated } -pub const STAKE_WARMUP_EPOCHS: Epoch = 3; + +pub const STAKE_WARMUP_RATE: f64 = 0.15; impl Default for Stake { fn default() -> Self { - Stake { + Self { voter_pubkey: Pubkey::default(), credits_observed: 0, stake: 0, @@ -68,31 +73,72 @@ impl Default for Stake { } impl Stake { - pub fn stake(&self, epoch: Epoch) -> u64 { - // before "activated" or after deactivated? - if epoch < self.activated || epoch >= self.deactivated { - return 0; - } + pub fn is_bootstrap(&self) -> bool { + self.activated == std::u64::MAX + } - // curr epoch | 0 | 1 | 2 ... | 100 | 101 | 102 | 103 - // action | activate | de-activate | | - // | | | | | | | | | - // | v | | | v | | | - // stake | 1/3 | 2/3 | 3/3 ... | 3/3 | 2/3 | 1/3 | 0/3 - // ------------------------------------------------------------- - // activated | 0 ... - // deactivated | std::u64::MAX ... 103 ... + pub fn activating(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 { + self.stake_and_activating(epoch, history).1 + } - // activate/deactivate can't possibly overlap - // (see delegate_stake() and deactivate()) - if epoch - self.activated < STAKE_WARMUP_EPOCHS { - // warmup - (self.stake / STAKE_WARMUP_EPOCHS) * (epoch - self.activated + 1) - } else if self.deactivated - epoch < STAKE_WARMUP_EPOCHS { - // cooldown - (self.stake / STAKE_WARMUP_EPOCHS) * (self.deactivated - epoch) + pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 { + self.stake_and_activating(epoch, history).0 + } + + pub fn stake_and_activating(&self, epoch: Epoch, history: Option<&StakeHistory>) -> (u64, u64) { + if epoch >= self.deactivated { + (0, 0) // TODO cooldown + } else if self.is_bootstrap() { + (self.stake, 0) + } else if epoch > self.activated { + if let Some(history) = history { + if let Some(mut entry) = history.get(&self.activated) { + let mut effective_stake = 0; + let mut next_epoch = self.activated; + + // loop from my activation epoch until the current epoch + // summing up my entitlement + loop { + if entry.activating == 0 { + break; + } + // how much of the growth in stake this account is + // entitled to take + let weight = + (self.stake - effective_stake) as f64 / entry.activating as f64; + + // portion of activating stake in this epoch I'm entitled to + effective_stake += + (weight * entry.effective as f64 * STAKE_WARMUP_RATE) as u64; + + if effective_stake >= self.stake { + effective_stake = self.stake; + break; + } + + next_epoch += 1; + if next_epoch >= epoch { + break; + } + if let Some(next_entry) = history.get(&next_epoch) { + entry = next_entry; + } else { + break; + } + } + (effective_stake, self.stake - effective_stake) + } else { + // I've dropped out of warmup history, so my stake must be the full amount + (self.stake, 0) + } + } else { + // no history, fully warmed up + (self.stake, 0) + } + } else if epoch == self.activated { + (0, self.stake) } else { - self.stake + (0, 0) } } @@ -106,6 +152,7 @@ impl Stake { &self, point_value: f64, vote_state: &VoteState, + stake_history: Option<&StakeHistory>, ) -> Option<(u64, u64, u64)> { if self.credits_observed >= vote_state.credits() { return None; @@ -128,12 +175,12 @@ impl Stake { 0 }; - total_rewards += (self.stake(*epoch) * epoch_credits) as f64 * point_value; + total_rewards += + (self.stake(*epoch, stake_history) * epoch_credits) as f64 * point_value; // don't want to assume anything about order of the iterator... - credits_observed = std::cmp::max(credits_observed, *credits); + credits_observed = credits_observed.max(*credits); } - // don't bother trying to collect fractional lamports if total_rewards < 1f64 { return None; @@ -153,23 +200,28 @@ impl Stake { )) } - fn delegate(&mut self, stake: u64, voter_pubkey: &Pubkey, vote_state: &VoteState, epoch: u64) { - assert!(std::u64::MAX - epoch >= (STAKE_WARMUP_EPOCHS * 2)); + fn new_bootstrap(stake: u64, voter_pubkey: &Pubkey, vote_state: &VoteState) -> Self { + Self { + stake, + activated: std::u64::MAX, + voter_pubkey: *voter_pubkey, + credits_observed: vote_state.credits(), + ..Stake::default() + } + } - // resets the current stake's credits - self.voter_pubkey = *voter_pubkey; - self.credits_observed = vote_state.credits(); - - // when this stake was activated - self.activated = epoch; - self.stake = stake; + fn new(stake: u64, voter_pubkey: &Pubkey, vote_state: &VoteState, activated: Epoch) -> Self { + Self { + stake, + activated, + voter_pubkey: *voter_pubkey, + credits_observed: vote_state.credits(), + ..Stake::default() + } } fn deactivate(&mut self, epoch: u64) { - self.deactivated = std::cmp::max( - epoch + STAKE_WARMUP_EPOCHS, - self.activated + 2 * STAKE_WARMUP_EPOCHS - 1, - ); + self.deactivated = epoch; } } @@ -190,12 +242,14 @@ pub trait StakeAccount { vote_account: &mut KeyedAccount, rewards_account: &mut KeyedAccount, rewards: &sysvar::rewards::Rewards, + stake_history: &sysvar::stake_history::StakeHistory, ) -> Result<(), InstructionError>; fn withdraw( &mut self, lamports: u64, to: &mut KeyedAccount, clock: &sysvar::clock::Clock, + stake_history: &sysvar::stake_history::StakeHistory, ) -> Result<(), InstructionError>; } @@ -215,9 +269,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { } if let StakeState::Uninitialized = self.state()? { - let mut stake = Stake::default(); - - stake.delegate( + let stake = Stake::new( new_stake, vote_account.unsigned_key(), &vote_account.state()?, @@ -251,6 +303,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { vote_account: &mut KeyedAccount, rewards_account: &mut KeyedAccount, rewards: &sysvar::rewards::Rewards, + stake_history: &sysvar::stake_history::StakeHistory, ) -> Result<(), InstructionError> { if let (StakeState::Stake(mut stake), StakeState::RewardsPool) = (self.state()?, rewards_account.state()?) @@ -261,8 +314,12 @@ impl<'a> StakeAccount for KeyedAccount<'a> { return Err(InstructionError::InvalidArgument); } - if let Some((stakers_reward, voters_reward, credits_observed)) = - stake.calculate_rewards(rewards.validator_point_value, &vote_state) + if let Some((stakers_reward, voters_reward, credits_observed)) = stake + .calculate_rewards( + rewards.validator_point_value, + &vote_state, + Some(stake_history), + ) { if rewards_account.account.lamports < (stakers_reward + voters_reward) { return Err(InstructionError::UnbalancedInstruction); @@ -288,16 +345,17 @@ impl<'a> StakeAccount for KeyedAccount<'a> { lamports: u64, to: &mut KeyedAccount, clock: &sysvar::clock::Clock, + stake_history: &sysvar::stake_history::StakeHistory, ) -> Result<(), InstructionError> { if self.signer_key().is_none() { return Err(InstructionError::MissingRequiredSignature); } match self.state()? { - StakeState::Stake(mut stake) => { + StakeState::Stake(stake) => { // if deactivated and in cooldown let staked = if clock.epoch >= stake.deactivated { - stake.stake(clock.epoch) + stake.stake(clock.epoch, Some(stake_history)) } else { // Assume full stake if the stake is under warmup, or // hasn't been de-activated @@ -308,9 +366,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { } self.account.lamports -= lamports; to.account.lamports += lamports; - // Adjust the stake (in case balance dropped below stake) - stake.stake = cmp::min(stake.stake, self.account.lamports); - self.set_state(&StakeState::Stake(stake)) + Ok(()) } StakeState::Uninitialized => { if lamports > self.account.lamports { @@ -325,6 +381,35 @@ impl<'a> StakeAccount for KeyedAccount<'a> { } } +//find_min<'a, I>(vals: I) -> Option<&'a u32> +//where +// I: Iterator, + +// utility function, used by runtime::Stakes, tests +pub fn new_stake_history_entry<'a, I>( + epoch: Epoch, + stakes: I, + history: Option<&StakeHistory>, +) -> StakeHistoryEntry +where + I: Iterator, +{ + // whatever the stake says they had for the epoch + // and whatever the were still waiting for + let (effective, activating): (Vec<_>, Vec<_>) = stakes + .map(|stake| stake.stake_and_activating(epoch, history)) + .unzip(); + + let effective = effective.iter().sum(); + let activating = activating.iter().sum(); + + StakeHistoryEntry { + effective, + activating, + ..StakeHistoryEntry::default() + } +} + // utility function, used by Bank, tests, genesis pub fn create_stake_account( voter_pubkey: &Pubkey, @@ -334,13 +419,11 @@ pub fn create_stake_account( let mut stake_account = Account::new(lamports, std::mem::size_of::(), &id()); stake_account - .set_state(&StakeState::Stake(Stake { - voter_pubkey: *voter_pubkey, - credits_observed: vote_state.credits(), - stake: lamports, - activated: 0, - deactivated: std::u64::MAX, - })) + .set_state(&StakeState::Stake(Stake::new_bootstrap( + lamports, + voter_pubkey, + vote_state, + ))) .expect("set_state"); stake_account @@ -361,6 +444,35 @@ mod tests { use solana_sdk::system_program; use solana_vote_api::vote_state; + fn create_stake_history_from_stakes( + bootstrap: Option, + epochs: std::ops::Range, + stakes: &[Stake], + ) -> StakeHistory { + let mut stake_history = StakeHistory::default(); + + let bootstrap_stake = if let Some(bootstrap) = bootstrap { + vec![Stake { + activated: std::u64::MAX, + stake: bootstrap, + ..Stake::default() + }] + } else { + vec![] + }; + + for epoch in epochs { + let entry = new_stake_history_entry( + epoch, + stakes.iter().chain(bootstrap_stake.iter()), + Some(&stake_history), + ); + stake_history.add(epoch, entry); + } + + stake_history + } + #[test] fn test_stake_delegate_stake() { let clock = sysvar::clock::Clock { @@ -439,27 +551,66 @@ mod tests { } #[test] - fn test_stake_stake() { - let mut stake = Stake::default(); - assert_eq!(stake.stake(0), 0); - let staked = STAKE_WARMUP_EPOCHS; - stake.delegate(staked, &Pubkey::default(), &VoteState::default(), 1); - // test warmup - for i in 0..STAKE_WARMUP_EPOCHS { - assert_eq!(stake.stake(i), i); - } - assert_eq!(stake.stake(STAKE_WARMUP_EPOCHS * 42), staked); + fn test_stake_warmup() { + let stakes = [ + Stake { + stake: 1_000, + activated: std::u64::MAX, + ..Stake::default() + }, + Stake { + stake: 1_000, + activated: 0, + ..Stake::default() + }, + Stake { + stake: 1_000, + activated: 1, + ..Stake::default() + }, + Stake { + stake: 1_000, + activated: 2, + ..Stake::default() + }, + Stake { + stake: 1_000, + activated: 2, + ..Stake::default() + }, + Stake { + stake: 1_000, + activated: 4, + ..Stake::default() + }, + ]; + // chosen to ensure that the last activated stake (at 4) finishes warming up + // a stake takes 2.0f64.log(1.0 + STAKE_WARMUP_RATE) epochs to warm up + // all else equal, but the above overlap + let epochs = 20; - stake.deactivate(STAKE_WARMUP_EPOCHS); + let stake_history = create_stake_history_from_stakes(None, 0..epochs, &stakes); - // test cooldown - for i in STAKE_WARMUP_EPOCHS..STAKE_WARMUP_EPOCHS * 2 { - assert_eq!( - stake.stake(i), - staked - (staked / STAKE_WARMUP_EPOCHS) * (i - STAKE_WARMUP_EPOCHS) - ); + let mut prev_total_effective_stake = stakes + .iter() + .map(|stake| stake.stake(0, Some(&stake_history))) + .sum::(); + + for epoch in 1.. { + let total_effective_stake = stakes + .iter() + .map(|stake| stake.stake(epoch, Some(&stake_history))) + .sum::(); + + let delta = total_effective_stake - prev_total_effective_stake; + + if delta == 0 { + break; + } + assert!(epoch < epochs); // should have warmed everything up by this time + assert!(delta as f64 / prev_total_effective_stake as f64 <= STAKE_WARMUP_RATE); + prev_total_effective_stake = total_effective_stake; } - assert_eq!(stake.stake(STAKE_WARMUP_EPOCHS * 42), 0); } #[test] @@ -528,14 +679,24 @@ mod tests { // unsigned keyed account should fail let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &mut stake_account); assert_eq!( - stake_keyed_account.withdraw(total_lamports, &mut to_keyed_account, &clock), + stake_keyed_account.withdraw( + total_lamports, + &mut to_keyed_account, + &clock, + &StakeHistory::default() + ), Err(InstructionError::MissingRequiredSignature) ); // signed keyed account and uninitialized should work let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); assert_eq!( - stake_keyed_account.withdraw(total_lamports, &mut to_keyed_account, &clock), + stake_keyed_account.withdraw( + total_lamports, + &mut to_keyed_account, + &clock, + &StakeHistory::default() + ), Ok(()) ); assert_eq!(stake_account.lamports, 0); @@ -546,7 +707,12 @@ mod tests { // signed keyed account and uninitialized, more than available should fail let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); assert_eq!( - stake_keyed_account.withdraw(total_lamports + 1, &mut to_keyed_account, &clock), + stake_keyed_account.withdraw( + total_lamports + 1, + &mut to_keyed_account, + &clock, + &StakeHistory::default() + ), Err(InstructionError::InsufficientFunds) ); @@ -567,7 +733,8 @@ mod tests { stake_keyed_account.withdraw( total_lamports - stake_lamports, &mut to_keyed_account, - &clock + &clock, + &StakeHistory::default() ), Ok(()) ); @@ -580,7 +747,8 @@ mod tests { stake_keyed_account.withdraw( total_lamports - stake_lamports + 1, &mut to_keyed_account, - &clock + &clock, + &StakeHistory::default() ), Err(InstructionError::InsufficientFunds) ); @@ -591,17 +759,27 @@ mod tests { Ok(()) ); // simulate time passing - clock.epoch += STAKE_WARMUP_EPOCHS * 2; + clock.epoch += 100; // Try to withdraw more than what's available assert_eq!( - stake_keyed_account.withdraw(total_lamports + 1, &mut to_keyed_account, &clock), + stake_keyed_account.withdraw( + total_lamports + 1, + &mut to_keyed_account, + &clock, + &StakeHistory::default() + ), Err(InstructionError::InsufficientFunds) ); // Try to withdraw all lamports assert_eq!( - stake_keyed_account.withdraw(total_lamports, &mut to_keyed_account, &clock), + stake_keyed_account.withdraw( + total_lamports, + &mut to_keyed_account, + &clock, + &StakeHistory::default() + ), Ok(()) ); assert_eq!(stake_account.lamports, 0); @@ -625,7 +803,7 @@ mod tests { let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); - // Stake some lamports (available lampoorts for withdrawls will reduce) + // Stake some lamports (available lampoorts for withdrawals will reduce) let vote_pubkey = Pubkey::new_rand(); let mut vote_account = vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100); @@ -636,12 +814,19 @@ mod tests { Ok(()) ); + let stake_history = create_stake_history_from_stakes( + None, + 0..future.epoch, + &[StakeState::stake_from(&stake_keyed_account.account).unwrap()], + ); + // Try to withdraw stake assert_eq!( stake_keyed_account.withdraw( total_lamports - stake_lamports + 1, &mut to_keyed_account, - &clock + &clock, + &stake_history ), Err(InstructionError::InsufficientFunds) ); @@ -667,7 +852,12 @@ mod tests { stake_keyed_account.set_state(&stake_state).unwrap(); assert_eq!( - stake_keyed_account.withdraw(total_lamports, &mut to_keyed_account, &clock), + stake_keyed_account.withdraw( + total_lamports, + &mut to_keyed_account, + &clock, + &StakeHistory::default() + ), Err(InstructionError::InvalidAccountData) ); } @@ -675,62 +865,62 @@ mod tests { #[test] fn test_stake_state_calculate_rewards() { let mut vote_state = VoteState::default(); - let mut stake = Stake::default(); - - // warmup makes this look like zero until WARMUP_EPOCHS - stake.stake = 1; + // assume stake.stake() is right + // bootstrap means fully-vested stake at epoch 0 + let mut stake = Stake::new_bootstrap(1, &Pubkey::default(), &vote_state); // this one can't collect now, credits_observed == vote_state.credits() - assert_eq!(None, stake.calculate_rewards(1_000_000_000.0, &vote_state)); + assert_eq!( + None, + stake.calculate_rewards(1_000_000_000.0, &vote_state, None) + ); // put 2 credits in at epoch 0 vote_state.increment_credits(0); vote_state.increment_credits(0); // this one can't collect now, no epoch credits have been saved off - assert_eq!(None, stake.calculate_rewards(1_000_000_000.0, &vote_state)); + // even though point value is huuge + assert_eq!( + None, + stake.calculate_rewards(1_000_000_000_000.0, &vote_state, None) + ); // put 1 credit in epoch 1, pushes the 2 above into a redeemable state vote_state.increment_credits(1); - // still can't collect yet, warmup puts the kibosh on it - assert_eq!(None, stake.calculate_rewards(1.0, &vote_state)); - - stake.stake = STAKE_WARMUP_EPOCHS; // this one should be able to collect exactly 2 assert_eq!( - Some((0, 1 * 2, 2)), - stake.calculate_rewards(1.0, &vote_state) + Some((0, stake.stake * 2, 2)), + stake.calculate_rewards(1.0, &vote_state, None) ); - stake.stake = STAKE_WARMUP_EPOCHS; stake.credits_observed = 1; // this one should be able to collect exactly 1 (only observed one) assert_eq!( - Some((0, 1 * 1, 2)), - stake.calculate_rewards(1.0, &vote_state) + Some((0, stake.stake * 1, 2)), + stake.calculate_rewards(1.0, &vote_state, None) ); - stake.stake = STAKE_WARMUP_EPOCHS; stake.credits_observed = 2; // this one should be able to collect none because credits_observed >= credits in a // redeemable state (the 2 credits in epoch 0) - assert_eq!(None, stake.calculate_rewards(1.0, &vote_state)); + assert_eq!(None, stake.calculate_rewards(1.0, &vote_state, None)); // put 1 credit in epoch 2, pushes the 1 for epoch 1 to redeemable vote_state.increment_credits(2); - // this one should be able to collect two now, one credit by a stake of 2 + // this one should be able to collect 1 now, one credit by a stake of 1 assert_eq!( - Some((0, 2 * 1, 3)), - stake.calculate_rewards(1.0, &vote_state) + Some((0, stake.stake * 1, 3)), + stake.calculate_rewards(1.0, &vote_state, None) ); 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((0, 2 * 1 + 1 * 2, 3)), - stake.calculate_rewards(1.0, &vote_state) + Some((0, stake.stake * 1 + stake.stake * 2, 3)), + stake.calculate_rewards(1.0, &vote_state, None) ); // same as above, but is a really small commission out of 32 bits, @@ -738,12 +928,12 @@ mod tests { vote_state.commission = 1; assert_eq!( None, // would be Some((0, 2 * 1 + 1 * 2, 3)), - stake.calculate_rewards(1.0, &vote_state) + stake.calculate_rewards(1.0, &vote_state, None) ); vote_state.commission = std::u8::MAX - 1; assert_eq!( None, // would be pSome((0, 2 * 1 + 1 * 2, 3)), - stake.calculate_rewards(1.0, &vote_state) + stake.calculate_rewards(1.0, &vote_state, None) ); } @@ -774,7 +964,8 @@ mod tests { stake_keyed_account.redeem_vote_credits( &mut vote_keyed_account, &mut rewards_pool_keyed_account, - &rewards + &rewards, + &StakeHistory::default(), ), Err(InstructionError::InvalidAccountData) ); @@ -783,22 +974,31 @@ mod tests { assert!(stake_keyed_account .delegate_stake(&vote_keyed_account, stake_lamports, &clock) .is_ok()); + + let stake_history = create_stake_history_from_stakes( + Some(100), + 0..10, + &[StakeState::stake_from(&stake_keyed_account.account).unwrap()], + ); + // no credits to claim assert_eq!( stake_keyed_account.redeem_vote_credits( &mut vote_keyed_account, &mut rewards_pool_keyed_account, - &rewards + &rewards, + &stake_history, ), Err(InstructionError::CustomError(1)) ); - // swapped rewards and vote, deserialization of rewards_pool fails + // in this call, we've swapped rewards and vote, deserialization of rewards_pool fails assert_eq!( stake_keyed_account.redeem_vote_credits( &mut rewards_pool_keyed_account, &mut vote_keyed_account, - &rewards + &rewards, + &StakeHistory::default(), ), Err(InstructionError::InvalidAccountData) ); @@ -809,9 +1009,9 @@ mod tests { let mut vote_state = VoteState::from(&vote_account).unwrap(); // put in some credits in epoch 0 for which we should have a non-zero stake for _i in 0..100 { - vote_state.increment_credits(0); + vote_state.increment_credits(1); } - vote_state.increment_credits(1); + vote_state.increment_credits(2); vote_state.to(&mut vote_account).unwrap(); let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account); @@ -822,7 +1022,8 @@ mod tests { stake_keyed_account.redeem_vote_credits( &mut vote_keyed_account, &mut rewards_pool_keyed_account, - &rewards + &rewards, + &StakeHistory::default(), ), Err(InstructionError::UnbalancedInstruction) ); @@ -833,7 +1034,8 @@ mod tests { stake_keyed_account.redeem_vote_credits( &mut vote_keyed_account, &mut rewards_pool_keyed_account, - &rewards + &rewards, + &stake_history, ), Ok(()) ); @@ -847,7 +1049,8 @@ mod tests { stake_keyed_account.redeem_vote_credits( &mut wrong_vote_keyed_account, &mut rewards_pool_keyed_account, - &rewards + &rewards, + &stake_history, ), Err(InstructionError::InvalidArgument) ); diff --git a/programs/stake_tests/tests/stake_instruction.rs b/programs/stake_tests/tests/stake_instruction.rs index db19382e69..5409b7be15 100644 --- a/programs/stake_tests/tests/stake_instruction.rs +++ b/programs/stake_tests/tests/stake_instruction.rs @@ -1,7 +1,7 @@ use assert_matches::assert_matches; use solana_runtime::bank::Bank; use solana_runtime::bank_client::BankClient; -use solana_runtime::genesis_utils::{create_genesis_block, GenesisBlockInfo}; +use solana_runtime::genesis_utils::{create_genesis_block_with_leader, GenesisBlockInfo}; use solana_sdk::account_utils::State; use solana_sdk::client::SyncClient; use solana_sdk::message::Message; @@ -63,7 +63,7 @@ fn test_stake_account_delegate() { mut genesis_block, mint_keypair, .. - } = create_genesis_block(100_000_000_000); + } = create_genesis_block_with_leader(100_000_000_000, &Pubkey::new_rand(), 1_000_000); genesis_block .native_instruction_processors .push(solana_stake_program::solana_stake_program!()); @@ -185,41 +185,43 @@ fn test_stake_account_delegate() { .send_message(&[&mint_keypair, &staker_keypair], message) .is_ok()); - // Test that we cannot withdraw staked lamports due to cooldown period - let message = Message::new_with_payer( - vec![stake_instruction::withdraw( - &staker_pubkey, - &Pubkey::new_rand(), - 20000, - )], - Some(&mint_pubkey), - ); - assert!(bank_client - .send_message(&[&mint_keypair, &staker_keypair], message) - .is_err()); - - let old_epoch = bank.epoch(); - let slots = bank.get_slots_in_epoch(old_epoch); - - // Create a new bank at later epoch (within cooldown period) - let bank = Bank::new_from_parent(&bank, &Pubkey::default(), slots + bank.slot()); - assert_ne!(old_epoch, bank.epoch()); - let bank = Arc::new(bank); - let bank_client = BankClient::new_shared(&bank); - - let message = Message::new_with_payer( - vec![stake_instruction::withdraw( - &staker_pubkey, - &Pubkey::new_rand(), - 20000, - )], - Some(&mint_pubkey), - ); - assert!(bank_client - .send_message(&[&mint_keypair, &staker_keypair], message) - .is_err()); + // // Test that we cannot withdraw staked lamports due to cooldown period + // let message = Message::new_with_payer( + // vec![stake_instruction::withdraw( + // &staker_pubkey, + // &Pubkey::new_rand(), + // 20000, + // )], + // Some(&mint_pubkey), + // ); + // assert!(bank_client + // .send_message(&[&mint_keypair, &staker_keypair], message) + // .is_err()); + // + // let old_epoch = bank.epoch(); + // let slots = bank.get_slots_in_epoch(old_epoch); + // + // // Create a new bank at later epoch (within cooldown period) + // let bank = Bank::new_from_parent(&bank, &Pubkey::default(), slots + bank.slot()); + // assert_ne!(old_epoch, bank.epoch()); + // let bank = Arc::new(bank); + // let bank_client = BankClient::new_shared(&bank); + // + // let message = Message::new_with_payer( + // vec![stake_instruction::withdraw( + // &staker_pubkey, + // &Pubkey::new_rand(), + // 20000, + // )], + // Some(&mint_pubkey), + // ); + // assert!(bank_client + // .send_message(&[&mint_keypair, &staker_keypair], message) + // .is_err()); + // TODO: implement cooldown // Create a new bank at later epoch (to account for cooldown of stake) + let mut bank = Bank::new_from_parent( &bank, &Pubkey::default(), @@ -245,9 +247,8 @@ fn test_stake_account_delegate() { // Test that balance and stake is updated correctly (we have withdrawn all lamports except rewards) let account = bank.get_account(&staker_pubkey).expect("account not found"); let stake_state = account.state().expect("couldn't unpack account data"); - if let StakeState::Stake(stake) = stake_state { + if let StakeState::Stake(_stake) = stake_state { assert_eq!(account.lamports, rewards); - assert_eq!(stake.stake, rewards); } else { assert!(false, "wrong account type found") } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 2dc67874e1..0225864ffc 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -27,22 +27,24 @@ use solana_measure::measure::Measure; use solana_metrics::{ datapoint_info, inc_new_counter_debug, inc_new_counter_error, inc_new_counter_info, }; -use solana_sdk::account::Account; -use solana_sdk::fee_calculator::FeeCalculator; -use solana_sdk::genesis_block::GenesisBlock; -use solana_sdk::hash::{hashv, Hash}; -use solana_sdk::inflation::Inflation; -use solana_sdk::native_loader; -use solana_sdk::pubkey::Pubkey; -use solana_sdk::signature::{Keypair, Signature}; -use solana_sdk::system_transaction; -use solana_sdk::sysvar::{ - clock, fees, rewards, - slot_hashes::{self, SlotHashes}, +use solana_sdk::{ + account::Account, + fee_calculator::FeeCalculator, + genesis_block::GenesisBlock, + hash::{hashv, Hash}, + inflation::Inflation, + native_loader, + pubkey::Pubkey, + signature::{Keypair, Signature}, + system_transaction, + sysvar::{ + clock, fees, rewards, + slot_hashes::{self, SlotHashes}, + stake_history, + }, + timing::{duration_as_ns, get_segment_from_slot, Epoch, Slot, MAX_RECENT_BLOCKHASHES}, + transaction::{Result, Transaction, TransactionError}, }; -use solana_sdk::timing::{duration_as_ns, get_segment_from_slot, Slot, MAX_RECENT_BLOCKHASHES}; -use solana_sdk::transaction::{Result, Transaction, TransactionError}; -use std::cmp; use std::collections::HashMap; use std::io::{BufReader, Cursor, Error as IOError, Read}; use std::path::Path; @@ -251,6 +253,7 @@ impl Bank { for epoch in 0..=bank.get_stakers_epoch(bank.slot) { bank.epoch_stakes.insert(epoch, stakes.clone()); } + bank.update_stake_history(None); } bank.update_clock(); bank @@ -330,6 +333,7 @@ impl Bank { }); new.update_rewards(parent.epoch()); + new.update_stake_history(Some(parent.epoch())); new.update_clock(); new.update_fees(); new @@ -401,8 +405,19 @@ impl Bank { self.store_account(&fees::id(), &fees::create_account(1, &self.fee_calculator)); } + fn update_stake_history(&self, epoch: Option) { + if epoch == Some(self.epoch()) { + return; + } + // if I'm the first Bank in an epoch, ensure stake_history is updated + self.store_account( + &stake_history::id(), + &stake_history::create_account(1, self.stakes.read().unwrap().history()), + ); + } + // update reward for previous epoch - fn update_rewards(&mut self, epoch: u64) { + fn update_rewards(&mut self, epoch: Epoch) { if epoch == self.epoch() { return; } @@ -626,7 +641,7 @@ impl Bank { if parents.is_empty() { self.last_blockhash_with_fee_calculator() } else { - let index = cmp::min(NUM_BLOCKHASH_CONFIRMATIONS, parents.len() - 1); + let index = NUM_BLOCKHASH_CONFIRMATIONS.min(parents.len() - 1); parents[index].last_blockhash_with_fee_calculator() } } @@ -1294,7 +1309,7 @@ impl Bank { } /// Return the number of slots per epoch for the given epoch - pub fn get_slots_in_epoch(&self, epoch: u64) -> u64 { + pub fn get_slots_in_epoch(&self, epoch: Epoch) -> u64 { self.epoch_schedule.get_slots_in_epoch(epoch) } @@ -1352,7 +1367,7 @@ impl Bank { /// vote accounts for the specific epoch along with the stake /// attributed to each account - pub fn epoch_vote_accounts(&self, epoch: u64) -> Option<&HashMap> { + pub fn epoch_vote_accounts(&self, epoch: Epoch) -> Option<&HashMap> { self.epoch_stakes.get(&epoch).map(Stakes::vote_accounts) } @@ -2428,6 +2443,7 @@ mod tests { let leader_stake = Stake { stake: leader_lamports, + activated: std::u64::MAX, // bootstrap ..Stake::default() }; @@ -2442,7 +2458,7 @@ mod tests { // epoch_stakes are a snapshot at the stakers_slot_offset boundary // in the prior epoch (0 in this case) assert_eq!( - leader_stake.stake(0), + leader_stake.stake(0, None), vote_accounts.unwrap().get(&leader_vote_account).unwrap().0 ); @@ -2458,7 +2474,7 @@ mod tests { assert!(child.epoch_vote_accounts(epoch).is_some()); assert_eq!( - leader_stake.stake(child.epoch()), + leader_stake.stake(child.epoch(), None), child .epoch_vote_accounts(epoch) .unwrap() @@ -2476,7 +2492,7 @@ mod tests { ); assert!(child.epoch_vote_accounts(epoch).is_some()); assert_eq!( - leader_stake.stake(child.epoch()), + leader_stake.stake(child.epoch(), None), child .epoch_vote_accounts(epoch) .unwrap() diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index d6deb53eee..d915a66b28 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -2,8 +2,9 @@ //! node stakes use solana_sdk::account::Account; use solana_sdk::pubkey::Pubkey; +use solana_sdk::sysvar::stake_history::StakeHistory; use solana_sdk::timing::Epoch; -use solana_stake_api::stake_state::StakeState; +use solana_stake_api::stake_state::{new_stake_history_entry, StakeState}; use solana_vote_api::vote_state::VoteState; use std::collections::HashMap; @@ -21,13 +22,36 @@ pub struct Stakes { /// current epoch, used to calculate current stake epoch: Epoch, + + /// history of staking levels + stake_history: StakeHistory, } impl Stakes { + pub fn history(&self) -> &StakeHistory { + &self.stake_history + } pub fn clone_with_epoch(&self, epoch: Epoch) -> Self { if self.epoch == epoch { self.clone() } else { + let mut stake_history = self.stake_history.clone(); + + stake_history.add( + self.epoch, + new_stake_history_entry( + self.epoch, + self.stake_accounts + .iter() + .filter_map(|(_pubkey, stake_account)| { + StakeState::stake_from(stake_account) + }) + .collect::>() + .iter(), + Some(&self.stake_history), + ), + ); + Stakes { stake_accounts: self.stake_accounts.clone(), points: self.points, @@ -38,22 +62,31 @@ impl Stakes { .map(|(pubkey, (_stake, account))| { ( *pubkey, - (self.calculate_stake(pubkey, epoch), account.clone()), + ( + self.calculate_stake(pubkey, epoch, Some(&stake_history)), + account.clone(), + ), ) }) .collect(), + stake_history, } } } // sum the stakes that point to the given voter_pubkey - fn calculate_stake(&self, voter_pubkey: &Pubkey, epoch: Epoch) -> u64 { + fn calculate_stake( + &self, + voter_pubkey: &Pubkey, + epoch: Epoch, + stake_history: Option<&StakeHistory>, + ) -> u64 { self.stake_accounts .iter() .map(|(_, stake_account)| { StakeState::stake_from(stake_account).map_or(0, |stake| { if stake.voter_pubkey == *voter_pubkey { - stake.stake(epoch) + stake.stake(epoch, stake_history) } else { 0 } @@ -75,7 +108,10 @@ impl Stakes { } else { let old = self.vote_accounts.get(pubkey); - let stake = old.map_or_else(|| self.calculate_stake(pubkey, self.epoch), |v| v.0); + let stake = old.map_or_else( + || self.calculate_stake(pubkey, self.epoch, Some(&self.stake_history)), + |v| v.0, + ); // count any increase in points, can only go forward let old_credits = old @@ -91,15 +127,19 @@ impl Stakes { } else if solana_stake_api::check_id(&account.owner) { // old_stake is stake lamports and voter_pubkey from the pre-store() version let old_stake = self.stake_accounts.get(pubkey).and_then(|old_account| { - StakeState::stake_from(old_account) - .map(|stake| (stake.voter_pubkey, stake.stake(self.epoch))) + StakeState::stake_from(old_account).map(|stake| { + ( + stake.voter_pubkey, + stake.stake(self.epoch, Some(&self.stake_history)), + ) + }) }); let stake = StakeState::stake_from(account).map(|stake| { ( stake.voter_pubkey, if account.lamports != 0 { - stake.stake(self.epoch) + stake.stake(self.epoch, Some(&self.stake_history)) } else { 0 }, @@ -165,7 +205,7 @@ impl Stakes { pub mod tests { use super::*; use solana_sdk::pubkey::Pubkey; - use solana_stake_api::stake_state::{self, STAKE_WARMUP_EPOCHS}; + use solana_stake_api::stake_state; use solana_vote_api::vote_state::{self, VoteState, MAX_LOCKOUT_HISTORY}; // set up some dummies for a staked node (( vote ) ( stake )) @@ -188,7 +228,7 @@ pub mod tests { #[test] fn test_stakes_basic() { - for i in 0..STAKE_WARMUP_EPOCHS + 1 { + for i in 0..4 { let mut stakes = Stakes::default(); stakes.epoch = i; @@ -201,7 +241,10 @@ pub mod tests { { let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); - assert_eq!(vote_accounts.get(&vote_pubkey).unwrap().0, stake.stake(i)); + assert_eq!( + vote_accounts.get(&vote_pubkey).unwrap().0, + stake.stake(i, None) + ); } stake_account.lamports = 42; @@ -209,7 +252,10 @@ pub mod tests { { let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); - assert_eq!(vote_accounts.get(&vote_pubkey).unwrap().0, stake.stake(i)); // stays old stake, because only 10 is activated + assert_eq!( + vote_accounts.get(&vote_pubkey).unwrap().0, + stake.stake(i, None) + ); // stays old stake, because only 10 is activated } // activate more @@ -219,7 +265,10 @@ pub mod tests { { let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); - assert_eq!(vote_accounts.get(&vote_pubkey).unwrap().0, stake.stake(i)); // now stake of 42 is activated + assert_eq!( + vote_accounts.get(&vote_pubkey).unwrap().0, + stake.stake(i, None) + ); // now stake of 42 is activated } stake_account.lamports = 0; @@ -258,7 +307,7 @@ pub mod tests { #[test] fn test_stakes_points() { let mut stakes = Stakes::default(); - stakes.epoch = STAKE_WARMUP_EPOCHS + 1; + stakes.epoch = 4; let stake = 42; assert_eq!(stakes.points(), 0); @@ -304,7 +353,7 @@ pub mod tests { #[test] fn test_stakes_vote_account_disappear_reappear() { let mut stakes = Stakes::default(); - stakes.epoch = STAKE_WARMUP_EPOCHS + 1; + stakes.epoch = 4; let ((vote_pubkey, mut vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10); @@ -338,7 +387,7 @@ pub mod tests { #[test] fn test_stakes_change_delegate() { let mut stakes = Stakes::default(); - stakes.epoch = STAKE_WARMUP_EPOCHS + 1; + stakes.epoch = 4; let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10); @@ -359,7 +408,7 @@ pub mod tests { assert!(vote_accounts.get(&vote_pubkey).is_some()); assert_eq!( vote_accounts.get(&vote_pubkey).unwrap().0, - stake.stake(stakes.epoch) + stake.stake(stakes.epoch, Some(&stakes.stake_history)) ); assert!(vote_accounts.get(&vote_pubkey2).is_some()); assert_eq!(vote_accounts.get(&vote_pubkey2).unwrap().0, 0); @@ -375,14 +424,14 @@ pub mod tests { assert!(vote_accounts.get(&vote_pubkey2).is_some()); assert_eq!( vote_accounts.get(&vote_pubkey2).unwrap().0, - stake.stake(stakes.epoch) + stake.stake(stakes.epoch, Some(&stakes.stake_history)) ); } } #[test] fn test_stakes_multiple_stakers() { let mut stakes = Stakes::default(); - stakes.epoch = STAKE_WARMUP_EPOCHS + 1; + stakes.epoch = 4; let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10); @@ -416,7 +465,7 @@ pub mod tests { let vote_accounts = stakes.vote_accounts(); assert_eq!( vote_accounts.get(&vote_pubkey).unwrap().0, - stake.stake(stakes.epoch) + stake.stake(stakes.epoch, Some(&stakes.stake_history)) ); } let stakes = stakes.clone_with_epoch(3); @@ -424,7 +473,7 @@ pub mod tests { let vote_accounts = stakes.vote_accounts(); assert_eq!( vote_accounts.get(&vote_pubkey).unwrap().0, - stake.stake(stakes.epoch) + stake.stake(stakes.epoch, Some(&stakes.stake_history)) ); } } @@ -432,7 +481,7 @@ pub mod tests { #[test] fn test_stakes_not_delegate() { let mut stakes = Stakes::default(); - stakes.epoch = STAKE_WARMUP_EPOCHS + 1; + stakes.epoch = 4; let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10); diff --git a/sdk/src/sysvar/stake_history.rs b/sdk/src/sysvar/stake_history.rs index a722f521e7..3b58c94172 100644 --- a/sdk/src/sysvar/stake_history.rs +++ b/sdk/src/sysvar/stake_history.rs @@ -19,14 +19,14 @@ crate::solana_name_id!(ID, "SysvarStakeHistory1111111111111111111111111"); pub const MAX_STAKE_HISTORY: usize = 512; // it should never take as many as 512 epochs to warm up or cool down -#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)] pub struct StakeHistoryEntry { - pub previous_effective: u64, // effective stake at the previous epoch - pub activating: u64, // requested to be warmed up, not fully activated yet - pub deactivating: u64, // requested to be cooled down, not fully deactivated yet + pub effective: u64, // effective stake at this epoch + pub activating: u64, // sum of portion of stakes not fully warmed up + pub deactivating: u64, // requested to be cooled down, not fully deactivated yet } -#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)] pub struct StakeHistory { inner: HashMap, } @@ -64,9 +64,9 @@ impl Deref for StakeHistory { } } -pub fn create_account(lamports: u64) -> Account { +pub fn create_account(lamports: u64, stake_history: &StakeHistory) -> Account { let mut account = Account::new(lamports, StakeHistory::size_of(), &sysvar::id()); - StakeHistory::default().to(&mut account).unwrap(); + stake_history.to(&mut account).unwrap(); account } @@ -86,7 +86,7 @@ mod tests { #[test] fn test_create_account() { let lamports = 42; - let account = create_account(lamports); + let account = create_account(lamports, &StakeHistory::default()); assert_eq!(account.data.len(), StakeHistory::size_of()); let stake_history = StakeHistory::from(&account); @@ -104,5 +104,10 @@ mod tests { } assert_eq!(stake_history.len(), MAX_STAKE_HISTORY); assert_eq!(*stake_history.keys().min().unwrap(), 1); + // verify the account can hold a full instance + assert_eq!( + StakeHistory::from(&create_account(lamports, &stake_history)), + Some(stake_history) + ); } }