stake cooldown (#5553)

* stake cooldown

* fixups

* sheesh
This commit is contained in:
Rob Walker 2019-08-17 18:12:30 -07:00 committed by GitHub
parent a252acf539
commit 814af378a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 179 additions and 97 deletions

View File

@ -166,7 +166,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
activation_epoch: std::u64::MAX, // mark as bootstrap
..Stake::default()
};
@ -200,7 +200,7 @@ pub(crate) mod tests {
// simulated stake
let other_stake = Stake {
stake,
activated: bank.epoch(),
activation_epoch: bank.epoch(),
..Stake::default()
};

View File

@ -53,9 +53,9 @@ impl StakeState {
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, std::Epoch::MAX if is a bootstrap stake
pub deactivated: Epoch, // epoch the stake was deactivated, std::Epoch::MAX if not deactivated
pub stake: u64, // stake amount activated
pub activation_epoch: Epoch, // epoch the stake was activated, std::Epoch::MAX if is a bootstrap stake
pub deactivation_epoch: Epoch, // epoch the stake was deactivated, std::Epoch::MAX if not deactivated
pub config: Config,
}
@ -65,8 +65,8 @@ impl Default for Stake {
voter_pubkey: Pubkey::default(),
credits_observed: 0,
stake: 0,
activated: 0,
deactivated: std::u64::MAX,
activation_epoch: 0,
deactivation_epoch: std::u64::MAX,
config: Config::default(),
}
}
@ -74,71 +74,128 @@ impl Default for Stake {
impl Stake {
pub fn is_bootstrap(&self) -> bool {
self.activated == std::u64::MAX
self.activation_epoch == std::u64::MAX
}
pub fn activating(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 {
self.stake_and_activating(epoch, history).1
self.stake_activating_and_deactivating(epoch, history).1
}
pub fn deactivating(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 {
self.stake_activating_and_deactivating(epoch, history).2
}
pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 {
self.stake_and_activating(epoch, history).0
self.stake_activating_and_deactivating(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;
pub fn stake_activating_and_deactivating(
&self,
epoch: Epoch,
history: Option<&StakeHistory>,
) -> (u64, u64, u64) {
// first, calculate an effective stake and activating number
let (stake, activating) = self.stake_and_activating(epoch, history);
// 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;
// then de-activate some portion if necessary
if epoch < self.deactivation_epoch {
(stake, activating, 0) // not deactivated
} else if epoch == self.deactivation_epoch {
(stake, 0, stake.min(self.stake)) // can only deactivate what's activated
} else if let Some((history, mut entry)) = history.and_then(|history| {
history
.get(&self.deactivation_epoch)
.map(|entry| (history, entry))
}) {
// && epoch > self.deactivation_epoch
let mut effective_stake = stake;
let mut next_epoch = self.deactivation_epoch;
// portion of activating stake in this epoch I'm entitled to
effective_stake +=
(weight * entry.effective as f64 * self.config.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)
// loop from my activation epoch until the current epoch
// summing up my entitlement
loop {
if entry.deactivating == 0 {
break;
}
// I'm trying to get to zero, how much of the deactivation in stake
// this account is entitled to take
let weight = effective_stake as f64 / entry.deactivating as f64;
// portion of activating stake in this epoch I'm entitled to
effective_stake = effective_stake.saturating_sub(
((weight * entry.effective as f64 * self.config.cooldown_rate) as u64).max(1),
);
if effective_stake == 0 {
break;
}
next_epoch += 1;
if next_epoch >= epoch {
break;
}
if let Some(next_entry) = history.get(&next_epoch) {
entry = next_entry;
} else {
break;
}
} else {
// no history, fully warmed up
(self.stake, 0)
}
} else if epoch == self.activated {
(0, self.stake)
(effective_stake, 0, effective_stake)
} else {
// no history or I've dropped out of history, so fully deactivated
(0, 0, 0)
}
}
fn stake_and_activating(&self, epoch: Epoch, history: Option<&StakeHistory>) -> (u64, u64) {
if self.is_bootstrap() {
(self.stake, 0)
} else if epoch == self.activation_epoch {
(0, self.stake)
} else if epoch < self.activation_epoch {
(0, 0)
} else if let Some((history, mut entry)) = history.and_then(|history| {
history
.get(&self.activation_epoch)
.map(|entry| (history, entry))
}) {
// && !is_bootstrap() && epoch > self.activation_epoch
let mut effective_stake = 0;
let mut next_epoch = self.activation_epoch;
// 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 * self.config.warmup_rate) as u64).max(1);
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 {
// no history or I've dropped out of history, so assume fully activated
(self.stake, 0)
}
}
@ -203,7 +260,7 @@ impl Stake {
fn new_bootstrap(stake: u64, voter_pubkey: &Pubkey, vote_state: &VoteState) -> Self {
Self {
stake,
activated: std::u64::MAX,
activation_epoch: std::u64::MAX,
voter_pubkey: *voter_pubkey,
credits_observed: vote_state.credits(),
..Stake::default()
@ -214,12 +271,12 @@ impl Stake {
stake: u64,
voter_pubkey: &Pubkey,
vote_state: &VoteState,
activated: Epoch,
activation_epoch: Epoch,
config: &Config,
) -> Self {
Self {
stake,
activated,
activation_epoch,
voter_pubkey: *voter_pubkey,
credits_observed: vote_state.credits(),
config: *config,
@ -228,7 +285,7 @@ impl Stake {
}
fn deactivate(&mut self, epoch: u64) {
self.deactivated = epoch;
self.deactivation_epoch = epoch;
}
}
@ -363,14 +420,16 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
match self.state()? {
StakeState::Stake(stake) => {
// if deactivated and in cooldown
let staked = if clock.epoch >= stake.deactivated {
// if we have a deactivation epoch and we're in cooldown
let staked = if clock.epoch >= stake.deactivation_epoch {
stake.stake(clock.epoch, Some(stake_history))
} else {
// Assume full stake if the stake is under warmup, or
// hasn't been de-activated
// Assume full stake if the stake account hasn't been
// de-activated, because in the future the exposeed stake
// might be higher than stake.stake(), 'cuz warmup
stake.stake
};
if lamports > self.account.lamports.saturating_sub(staked) {
return Err(InstructionError::InsufficientFunds);
}
@ -402,17 +461,17 @@ where
{
// 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();
fn add(a: (u64, u64, u64), b: (u64, u64, u64)) -> (u64, u64, u64) {
(a.0 + b.0, a.1 + b.1, a.2 + b.2)
}
let (effective, activating, deactivating) = stakes.fold((0, 0, 0), |sum, stake| {
add(sum, stake.stake_activating_and_deactivating(epoch, history))
});
StakeHistoryEntry {
effective,
activating,
..StakeHistoryEntry::default()
deactivating,
}
}
@ -452,7 +511,7 @@ mod tests {
let bootstrap_stake = if let Some(bootstrap) = bootstrap {
vec![Stake {
activated: std::u64::MAX,
activation_epoch: std::u64::MAX,
stake: bootstrap,
..Stake::default()
}]
@ -528,8 +587,8 @@ mod tests {
voter_pubkey: vote_keypair.pubkey(),
credits_observed: vote_state.credits(),
stake: stake_lamports,
activated: clock.epoch,
deactivated: std::u64::MAX,
activation_epoch: clock.epoch,
deactivation_epoch: std::u64::MAX,
config: Config::default()
})
);
@ -569,39 +628,46 @@ mod tests {
fn test_stake_warmup() {
let stakes = [
Stake {
// never deactivates
stake: 1_000,
activated: std::u64::MAX,
activation_epoch: std::u64::MAX,
..Stake::default()
},
Stake {
stake: 1_000,
activated: 0,
activation_epoch: 0,
deactivation_epoch: 9,
..Stake::default()
},
Stake {
stake: 1_000,
activated: 1,
activation_epoch: 1,
deactivation_epoch: 6,
..Stake::default()
},
Stake {
stake: 1_000,
activated: 2,
activation_epoch: 2,
deactivation_epoch: 5,
..Stake::default()
},
Stake {
stake: 1_000,
activated: 2,
activation_epoch: 2,
deactivation_epoch: 4,
..Stake::default()
},
Stake {
stake: 1_000,
activated: 4,
activation_epoch: 4,
deactivation_epoch: 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
// chosen to ensure that the last activated stake (at 4) finishes
// warming up and cooling down
// a stake takes 2.0f64.log(1.0 + STAKE_WARMUP_RATE) epochs to warm up or cool down
// when all alone, but the above overlap a lot
let epochs = 20;
let stake_history = create_stake_history_from_stakes(None, 0..epochs, &stakes);
@ -611,21 +677,31 @@ mod tests {
.map(|stake| stake.stake(0, Some(&stake_history)))
.sum::<u64>();
for epoch in 1.. {
// uncomment and add ! for fun with graphing
// eprintln("\n{:8} {:8} {:8}", " epoch", " total", " delta");
for epoch in 1..epochs {
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;
let delta = if total_effective_stake > prev_total_effective_stake {
total_effective_stake - prev_total_effective_stake
} else {
prev_total_effective_stake - total_effective_stake
};
// uncomment and add ! for fun with graphing
//eprint("{:8} {:8} {:8} ", epoch, total_effective_stake, delta);
//(0..(total_effective_stake as usize / (stakes.len() * 5))).for_each(|_| eprint("#"));
//eprintln();
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 <= Config::default().warmup_rate
delta
<= ((prev_total_effective_stake as f64 * Config::default().warmup_rate) as u64)
.max(1)
);
prev_total_effective_stake = total_effective_stake;
}
}
@ -765,7 +841,7 @@ mod tests {
),
Ok(())
);
// reset
// reset balance
stake_account.lamports = total_lamports;
// withdrawal before deactivate fails if not in excess of stake

View File

@ -2451,7 +2451,7 @@ mod tests {
let leader_stake = Stake {
stake: leader_lamports,
activated: std::u64::MAX, // bootstrap
activation_epoch: std::u64::MAX, // bootstrap
..Stake::default()
};

View File

@ -864,9 +864,15 @@ fn process_show_stake_account(
if stake.voter_pubkey != Pubkey::default() {
println!("delegated voter pubkey: {}", stake.voter_pubkey);
}
println!("stake activates at epoch: {}", stake.activated);
if stake.deactivated < std::u64::MAX {
println!("stake deactivates at epoch: {}", stake.deactivated);
println!(
"stake activates starting from epoch: {}",
stake.activation_epoch
);
if stake.deactivation_epoch < std::u64::MAX {
println!(
"stake deactivates starting from epoch: {}",
stake.deactivation_epoch
);
}
Ok("".to_string())
}