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