diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 9f394decc..bfeb1d9c9 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -365,13 +365,28 @@ impl Stake { pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 { self.delegation.stake(epoch, history) } + + pub fn redeem_rewards( + &mut self, + point_value: f64, + vote_state: &VoteState, + stake_history: Option<&StakeHistory>, + ) -> Option<(u64, u64)> { + self.calculate_rewards(point_value, vote_state, stake_history) + .map(|(voters_reward, stakers_reward, credits_observed)| { + self.credits_observed = credits_observed; + self.delegation.stake += stakers_reward; + (voters_reward, stakers_reward) + }) + } + /// for a given stake and vote_state, calculate what distributions and what updates should be made /// returns a tuple in the case of a payout of: /// * voter_rewards to be distributed /// * staker_rewards to be distributed /// * new value for credits_observed in the stake // returns None if there's no payout or if any deserved payout is < 1 lamport - fn calculate_rewards( + pub fn calculate_rewards( &self, point_value: f64, vote_state: &VoteState, @@ -805,6 +820,33 @@ impl<'a> StakeAccount for KeyedAccount<'a> { } } +// utility function, used by runtime +pub fn redeem_rewards( + stake_account: &mut Account, + vote_account: &mut Account, + point_value: f64, + stake_history: Option<&StakeHistory>, +) -> Result { + if let StakeState::Stake(meta, mut stake) = stake_account.state()? { + let vote_state = vote_account.state()?; + + if let Some((voters_reward, stakers_reward)) = + stake.redeem_rewards(point_value, &vote_state, stake_history) + { + stake_account.lamports += stakers_reward; + vote_account.lamports += voters_reward; + + stake_account.set_state(&StakeState::Stake(meta, stake))?; + + Ok(stakers_reward + voters_reward) + } else { + Err(StakeError::NoCreditsToRedeem.into()) + } + } else { + Err(InstructionError::InvalidAccountData) + } +} + // utility function, used by runtime::Stakes, tests pub fn new_stake_history_entry<'a, I>( epoch: Epoch, @@ -1924,6 +1966,43 @@ mod tests { ); } + #[test] + fn test_stake_state_redeem_rewards() { + let mut vote_state = VoteState::default(); + // assume stake.stake() is right + // bootstrap means fully-vested stake at epoch 0 + let stake_lamports = 1; + let mut stake = Stake::new( + stake_lamports, + &Pubkey::default(), + &vote_state, + std::u64::MAX, + &Config::default(), + ); + + // this one can't collect now, credits_observed == vote_state.credits() + assert_eq!( + None, + stake.redeem_rewards(1_000_000_000.0, &vote_state, None) + ); + + // put 2 credits in at epoch 0 + vote_state.increment_credits(0); + vote_state.increment_credits(0); + + // this one should be able to collect exactly 2 + assert_eq!( + Some((0, stake_lamports * 2)), + stake.redeem_rewards(1.0, &vote_state, None) + ); + + assert_eq!( + stake.delegation.stake, + stake_lamports + (stake_lamports * 2) + ); + assert_eq!(stake.credits_observed, 2); + } + #[test] fn test_stake_state_calculate_rewards() { let mut vote_state = VoteState::default(); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 2d7a4cbf4..4335b5953 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -48,7 +48,7 @@ use solana_sdk::{ timing::years_as_slots, transaction::{Result, Transaction, TransactionError}, }; -use solana_stake_program::stake_state::Delegation; +use solana_stake_program::stake_state::{self, Delegation}; use solana_vote_program::vote_state::VoteState; use std::{ cell::RefCell, @@ -630,12 +630,50 @@ impl Bank { &sysvar::rewards::create_account(1, validator_point_value, storage_point_value), ); + let validator_rewards = self.pay_validator_rewards(validator_point_value); + self.capitalization.fetch_add( - (validator_rewards + storage_rewards) as u64, + validator_rewards + storage_rewards as u64, Ordering::Relaxed, ); } + /// iterate over all stakes, redeem vote credits for each stake we can + /// successfully load and parse, return total payout + fn pay_validator_rewards(&self, point_value: f64) -> u64 { + let stake_history = self.stakes.read().unwrap().history().clone(); + self.stake_delegations() + .iter() + .map(|(stake_pubkey, delegation)| { + match ( + self.get_account(&stake_pubkey), + self.get_account(&delegation.voter_pubkey), + ) { + (Some(mut stake_account), Some(mut vote_account)) => { + let rewards = stake_state::redeem_rewards( + &mut stake_account, + &mut vote_account, + point_value, + Some(&stake_history), + ); + if let Ok(rewards) = rewards { + self.store_account(&stake_pubkey, &stake_account); + self.store_account(&delegation.voter_pubkey, &vote_account); + rewards + } else { + debug!( + "stake_state::redeem_rewards() failed for {}: {:?}", + stake_pubkey, rewards + ); + 0 + } + } + (_, _) => 0, + } + }) + .sum() + } + pub fn update_recent_blockhashes(&self) { let blockhash_queue = self.blockhash_queue.read().unwrap(); let recent_blockhash_iter = blockhash_queue.get_recent_blockhashes(); @@ -2920,14 +2958,14 @@ mod tests { })); assert_eq!(bank.capitalization(), 42 * 1_000_000_000); - let ((vote_id, mut vote_account), stake) = + let ((vote_id, mut vote_account), (stake_id, stake_account)) = crate::stakes::tests::create_staked_node_accounts(1_0000); let ((validator_id, validator_account), (archiver_id, archiver_account)) = crate::storage_utils::tests::create_storage_accounts_with_credits(100); // set up stakes, vote, and storage accounts - bank.store_account(&stake.0, &stake.1); + bank.store_account(&stake_id, &stake_account); bank.store_account(&validator_id, &validator_account.borrow()); bank.store_account(&archiver_id, &archiver_account.borrow()); @@ -2960,6 +2998,16 @@ mod tests { .map(|account| Rewards::from_account(&account).unwrap()) .unwrap(); + // verify the stake and vote accounts are the right size + assert!( + ((bank1.get_balance(&stake_id) - stake_account.lamports + bank1.get_balance(&vote_id) + - vote_account.lamports) as f64 + - rewards.validator_point_value * validator_points as f64) + .abs() + < 1.0 + ); + + // verify the rewards are the right size assert!( ((rewards.validator_point_value * validator_points as f64 + rewards.storage_point_value * storage_points as f64) diff --git a/runtime/tests/stake.rs b/runtime/tests/stake.rs index c5f7443ce..5ffc64bd5 100644 --- a/runtime/tests/stake.rs +++ b/runtime/tests/stake.rs @@ -1,4 +1,3 @@ -use assert_matches::assert_matches; use solana_runtime::{ bank::Bank, bank_client::BankClient, @@ -11,7 +10,7 @@ use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, KeypairUtil}, system_instruction::create_address_with_seed, - sysvar::{self, rewards::Rewards, stake_history::StakeHistory, Sysvar}, + sysvar::{self, stake_history::StakeHistory, Sysvar}, }; use solana_stake_program::{ stake_instruction::{self}, @@ -241,6 +240,14 @@ fn test_stake_account_lifetime() { assert!(false, "wrong account type found") } + loop { + if warmed_up(&bank, &stake_pubkey) { + break; + } + // Cycle thru banks until we're fully warmed up + bank = next_epoch(&bank); + } + // Reward redemption // Submit enough votes to generate rewards bank = fill_epoch_with_votes(&bank, &vote_keypair, &mint_keypair); @@ -251,35 +258,14 @@ fn test_stake_account_lifetime() { // 1 less vote, as the first vote should have cleared the lockout assert_eq!(vote_state.votes.len(), 31); - assert_eq!(vote_state.credits(), 1); + // one vote per slot, might be more slots than 32 in the epoch + assert!(vote_state.credits() >= 1); bank = fill_epoch_with_votes(&bank, &vote_keypair, &mint_keypair); - loop { - if warmed_up(&bank, &stake_pubkey) { - break; - } - // Cycle thru banks until we're fully warmed up - bank = next_epoch(&bank); - } - - // Test that rewards are there - let rewards_account = bank - .get_account(&sysvar::rewards::id()) - .expect("account not found"); - assert_matches!(Rewards::from_account(&rewards_account), Some(_)); - let pre_staked = get_staked(&bank, &stake_pubkey); - // Redeem the credit - let bank_client = BankClient::new_shared(&bank); - let message = Message::new_with_payer( - vec![stake_instruction::redeem_vote_credits( - &stake_pubkey, - &vote_pubkey, - )], - Some(&mint_pubkey), - ); - assert_matches!(bank_client.send_message(&[&mint_keypair], message), Ok(_)); + // next epoch bank should pay rewards + bank = next_epoch(&bank); // Test that balance increased, and that the balance got staked let staked = get_staked(&bank, &stake_pubkey); @@ -290,6 +276,8 @@ fn test_stake_account_lifetime() { // split the stake let split_stake_keypair = Keypair::new(); let split_stake_pubkey = split_stake_keypair.pubkey(); + + let bank_client = BankClient::new_shared(&bank); // Test split let message = Message::new_with_payer( stake_instruction::split( diff --git a/sdk/src/account_utils.rs b/sdk/src/account_utils.rs index 5a47ac6c4..6b7edd5d7 100644 --- a/sdk/src/account_utils.rs +++ b/sdk/src/account_utils.rs @@ -1,6 +1,8 @@ //! useful extras for Account state -use crate::account::{Account, KeyedAccount}; -use crate::instruction::InstructionError; +use crate::{ + account::{Account, KeyedAccount}, + instruction::InstructionError, +}; use bincode::ErrorKind; /// Convenience trait to covert bincode errors to instruction errors.