diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 30102289c5..b5032b0d82 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -2381,7 +2381,13 @@ pub mod tests { assert_ne!(leader_info.activated_stake, 0); // Subtract one because the last vote always carries over to the next epoch let expected_credits = TEST_SLOTS_PER_EPOCH - MAX_LOCKOUT_HISTORY as u64 - 1; - assert_eq!(leader_info.epoch_credits, vec![(0, expected_credits, 0)]); + assert_eq!( + leader_info.epoch_credits, + vec![ + (0, expected_credits, 0), + (1, expected_credits + 1, expected_credits) // one vote in current epoch + ] + ); // Advance bank with no voting bank.freeze(); diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index aaffaf8646..30628e285f 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -1933,16 +1933,6 @@ mod tests { vote_state.increment_credits(0); vote_state.increment_credits(0); - // this one can't collect now, no epoch credits have been saved off - // 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); - // this one should be able to collect exactly 2 assert_eq!( Some((0, stake.delegation.stake * 2, 2)), @@ -1950,33 +1940,40 @@ mod tests { ); 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 (already observed one) assert_eq!( Some((0, stake.delegation.stake * 1, 2)), stake.calculate_rewards(1.0, &vote_state, None) ); - 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, None)); + // put 1 credit in epoch 1 + vote_state.increment_credits(1); - // 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 1 now, one credit by a stake of 1 + stake.credits_observed = 2; + // this one should be able to collect the one just added assert_eq!( Some((0, stake.delegation.stake * 1, 3)), stake.calculate_rewards(1.0, &vote_state, None) ); + // put 1 credit in epoch 2 + vote_state.increment_credits(2); + // this one should be able to collect 2 now + assert_eq!( + Some((0, stake.delegation.stake * 2, 4)), + 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, - stake.delegation.stake * 1 + stake.delegation.stake * 2, - 3 + stake.delegation.stake * 2 // epoch 0 + + stake.delegation.stake * 1 // epoch 1 + + stake.delegation.stake * 1, // epoch 2 + 4 )), stake.calculate_rewards(1.0, &vote_state, None) ); @@ -1985,12 +1982,12 @@ mod tests { // verify that None comes back on small redemptions where no one gets paid vote_state.commission = 1; assert_eq!( - None, // would be Some((0, 2 * 1 + 1 * 2, 3)), + None, // would be Some((0, 2 * 1 + 1 * 2, 4)), stake.calculate_rewards(1.0, &vote_state, None) ); vote_state.commission = 99; assert_eq!( - None, // would be Some((0, 2 * 1 + 1 * 2, 3)), + None, // would be Some((0, 2 * 1 + 1 * 2, 4)), stake.calculate_rewards(1.0, &vote_state, None) ); } diff --git a/programs/vote/src/vote_state.rs b/programs/vote/src/vote_state.rs index c1bb0f2fea..50950b6d81 100644 --- a/programs/vote/src/vote_state.rs +++ b/programs/vote/src/vote_state.rs @@ -151,14 +151,6 @@ pub struct VoteState { pub votes: VecDeque, pub root_slot: Option, - /// clock epoch - epoch: Epoch, - /// clock credits earned, monotonically increasing - credits: u64, - - /// credits as of previous epoch - last_epoch_credits: u64, - /// history of how many credits earned by the end of each epoch /// each tuple is (Epoch, credits, prev_credits) epoch_credits: Vec<(Epoch, u64, u64)>, @@ -322,31 +314,37 @@ impl VoteState { /// increment credits, record credits for last epoch if new epoch pub fn increment_credits(&mut self, epoch: Epoch) { - // record credits by epoch + // increment credits, record by epoch - if epoch != self.epoch { - // encode the delta, but be able to return partial for stakers who - // attach halfway through an epoch - if self.credits > 0 { - self.epoch_credits - .push((self.epoch, self.credits, self.last_epoch_credits)); + // never seen a credit + if self.epoch_credits.is_empty() { + self.epoch_credits.push((epoch, 0, 0)); + } else if epoch != self.epoch_credits.last().unwrap().0 { + let (_, credits, prev_credits) = *self.epoch_credits.last().unwrap(); + + if credits != prev_credits { + // if credits were earned previous epoch + // append entry at end of list for the new epoch + self.epoch_credits.push((epoch, credits, credits)); + } else { + // else just move the current epoch + self.epoch_credits.last_mut().unwrap().0 = epoch; } + // if stakers do not claim before the epoch goes away they lose the // credits... if self.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY { self.epoch_credits.remove(0); } - self.epoch = epoch; - self.last_epoch_credits = self.credits; } - self.credits += 1; + self.epoch_credits.last_mut().unwrap().1 += 1; } /// "unchecked" functions used by tests and Tower pub fn process_vote_unchecked(&mut self, vote: &Vote) { let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect(); - let _ignored = self.process_vote(vote, &slot_hashes, self.epoch); + let _ignored = self.process_vote(vote, &slot_hashes, self.current_epoch()); } pub fn process_slot_vote_unchecked(&mut self, slot: Slot) { self.process_vote_unchecked(&Vote::new(vec![slot], Hash::default())); @@ -361,10 +359,22 @@ impl VoteState { } } + fn current_epoch(&self) -> Epoch { + if self.epoch_credits.is_empty() { + 0 + } else { + self.epoch_credits.last().unwrap().0 + } + } + /// Number of "credits" owed to this account from the mining pool. Submit this /// VoteState to the Rewards program to trade credits for lamports. pub fn credits(&self) -> u64 { - self.credits + if self.epoch_credits.is_empty() { + 0 + } else { + self.epoch_credits.last().unwrap().1 + } } /// Number of "credits" owed to this account from the mining pool on a per-epoch basis, @@ -1022,10 +1032,10 @@ mod tests { vote_state.process_slot_vote_unchecked(i as u64); } - assert_eq!(vote_state.credits, 0); + assert_eq!(vote_state.credits(), 0); vote_state.process_slot_vote_unchecked(MAX_LOCKOUT_HISTORY as u64 + 1); - assert_eq!(vote_state.credits, 1); + assert_eq!(vote_state.credits(), 1); vote_state.process_slot_vote_unchecked(MAX_LOCKOUT_HISTORY as u64 + 2); assert_eq!(vote_state.credits(), 2); vote_state.process_slot_vote_unchecked(MAX_LOCKOUT_HISTORY as u64 + 3); @@ -1329,7 +1339,7 @@ mod tests { } expected.push((epoch, credits, credits - epoch)); } - expected.pop(); // last one doesn't count, doesn't get saved off + while expected.len() > MAX_EPOCH_CREDITS_HISTORY { expected.remove(0); } @@ -1344,10 +1354,10 @@ mod tests { assert_eq!(vote_state.epoch_credits().len(), 0); vote_state.increment_credits(1); - assert_eq!(vote_state.epoch_credits().len(), 0); + assert_eq!(vote_state.epoch_credits().len(), 1); vote_state.increment_credits(2); - assert_eq!(vote_state.epoch_credits().len(), 1); + assert_eq!(vote_state.epoch_credits().len(), 2); } #[test]