diff --git a/core/src/staking_utils.rs b/core/src/staking_utils.rs index 0053cc1983..d05c44e5d4 100644 --- a/core/src/staking_utils.rs +++ b/core/src/staking_utils.rs @@ -145,7 +145,6 @@ pub(crate) mod tests { let leader_stake = Stake { stake: BOOTSTRAP_LEADER_LAMPORTS, - epoch: 0, ..Stake::default() }; diff --git a/programs/stake_api/src/rewards_pools.rs b/programs/stake_api/src/rewards_pools.rs index 01b027530b..a87f2df54a 100644 --- a/programs/stake_api/src/rewards_pools.rs +++ b/programs/stake_api/src/rewards_pools.rs @@ -3,9 +3,8 @@ //! * keep track of rewards //! * own mining pools -use crate::stake_state::StakeState; +use crate::stake_state::create_rewards_pool; use rand::{thread_rng, Rng}; -use solana_sdk::account::Account; use solana_sdk::genesis_block::Builder; use solana_sdk::hash::{hash, Hash}; use solana_sdk::pubkey::Pubkey; @@ -25,10 +24,7 @@ pub fn genesis(mut builder: Builder) -> Builder { let mut pubkey = id(); for _i in 0..NUM_REWARDS_POOLS { - builder = builder.rewards_pool( - pubkey, - Account::new_data(std::u64::MAX, &StakeState::RewardsPool, &crate::id()).unwrap(), - ); + builder = builder.rewards_pool(pubkey, create_rewards_pool()); pubkey = Pubkey::new(hash(pubkey.as_ref()).as_ref()); } builder diff --git a/programs/stake_api/src/stake_instruction.rs b/programs/stake_api/src/stake_instruction.rs index ed1efedd3a..f81068c5e5 100644 --- a/programs/stake_api/src/stake_instruction.rs +++ b/programs/stake_api/src/stake_instruction.rs @@ -11,30 +11,25 @@ use solana_sdk::system_instruction; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum StakeInstruction { - // Initialize the stake account as a MiningPool account - /// - /// Expects 1 Accounts: - /// 0 - MiningPool StakeAccount to be initialized - InitializeMiningPool, - /// `Delegate` a stake to a particular node /// - /// Expects 2 Accounts: + /// Expects 3 Accounts: /// 0 - Uninitialized StakeAccount to be delegated <= must have this signature /// 1 - VoteAccount to which this Stake will be delegated + /// 2 - Current syscall Account that carries current bank epoch /// /// The u64 is the portion of the Stake account balance to be activated, /// must be less than StakeAccount.lamports /// - /// This instruction resets rewards, so issue DelegateStake(u64), /// Redeem credits in the stake account /// - /// Expects 3 Accounts: - /// 0 - MiningPool Stake Account to redeem credits from - /// 1 - Delegate StakeAccount to be updated - /// 2 - VoteAccount to which the Stake is delegated, + /// Expects 4 Accounts: + /// 0 - Delegate StakeAccount to be updated with rewards + /// 1 - VoteAccount to which the Stake is delegated, + /// 2 - RewardsPool Stake Account from which to redeem credits + /// 3 - Rewards syscall Account that carries points values RedeemVoteCredits, } @@ -63,36 +58,12 @@ pub fn create_stake_account_and_delegate_stake( instructions } -pub fn create_mining_pool_account( - from_pubkey: &Pubkey, - staker_pubkey: &Pubkey, - lamports: u64, -) -> Vec { - vec![ - system_instruction::create_account( - from_pubkey, - staker_pubkey, - lamports, - std::mem::size_of::() as u64, - &id(), - ), - Instruction::new( - id(), - &StakeInstruction::InitializeMiningPool, - vec![AccountMeta::new(*staker_pubkey, false)], - ), - ] -} - -pub fn redeem_vote_credits( - mining_pool_pubkey: &Pubkey, - stake_pubkey: &Pubkey, - vote_pubkey: &Pubkey, -) -> Instruction { +pub fn redeem_vote_credits(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey) -> Instruction { let account_metas = vec![ - AccountMeta::new(*mining_pool_pubkey, false), AccountMeta::new(*stake_pubkey, false), AccountMeta::new(*vote_pubkey, false), + AccountMeta::new(crate::rewards_pools::random_id(), false), + AccountMeta::new(syscall::rewards::id(), false), ]; Instruction::new(id(), &StakeInstruction::RedeemVoteCredits, account_metas) } @@ -106,11 +77,6 @@ pub fn delegate_stake(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey, stake: u64) - Instruction::new(id(), &StakeInstruction::DelegateStake(stake), account_metas) } -fn current(current_account: &KeyedAccount) -> Result { - syscall::current::Current::from(current_account.account) - .ok_or(InstructionError::InvalidArgument) -} - pub fn process_instruction( _program_id: &Pubkey, keyed_accounts: &mut [KeyedAccount], @@ -130,29 +96,32 @@ pub fn process_instruction( // TODO: data-driven unpack and dispatch of KeyedAccounts match deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)? { - StakeInstruction::InitializeMiningPool => { - if !rest.is_empty() { - Err(InstructionError::InvalidInstructionData)?; - } - me.initialize_mining_pool() - } StakeInstruction::DelegateStake(stake) => { if rest.len() != 2 { Err(InstructionError::InvalidInstructionData)?; } let vote = &rest[0]; - me.delegate_stake(vote, stake, ¤t(&rest[1])?) + me.delegate_stake( + vote, + stake, + &syscall::current::from_keyed_account(&rest[1])?, + ) } StakeInstruction::RedeemVoteCredits => { - if rest.len() != 2 { + if rest.len() != 3 { Err(InstructionError::InvalidInstructionData)?; } - let (stake, vote) = rest.split_at_mut(1); - let stake = &mut stake[0]; + let (vote, rest) = rest.split_at_mut(1); let vote = &mut vote[0]; + let (rewards_pool, rest) = rest.split_at_mut(1); + let rewards_pool = &mut rewards_pool[0]; - me.redeem_vote_credits(stake, vote) + me.redeem_vote_credits( + vote, + rewards_pool, + &syscall::rewards::from_keyed_account(&rest[0])?, + ) } } } @@ -170,6 +139,8 @@ mod tests { .map(|meta| { if syscall::current::check_id(&meta.pubkey) { syscall::current::create_account(1, 0, 0, 0) + } else if syscall::rewards::check_id(&meta.pubkey) { + syscall::rewards::create_account(1, 0.0, 0.0) } else { Account::default() } @@ -190,11 +161,7 @@ mod tests { #[test] fn test_stake_process_instruction() { assert_eq!( - process_instruction(&redeem_vote_credits( - &Pubkey::default(), - &Pubkey::default(), - &Pubkey::default() - )), + process_instruction(&redeem_vote_credits(&Pubkey::default(), &Pubkey::default(),)), Err(InstructionError::InvalidAccountData), ); assert_eq!( @@ -265,7 +232,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); - // gets the check in redeem_vote_credits + // gets the deserialization checks in redeem_vote_credits assert_eq!( super::process_instruction( &Pubkey::default(), @@ -273,6 +240,11 @@ mod tests { KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()), KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()), KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()), + KeyedAccount::new( + &syscall::rewards::id(), + false, + &mut syscall::rewards::create_account(1, 0.0, 0.0) + ), ], &serialize(&StakeInstruction::RedeemVoteCredits).unwrap(), ), diff --git a/programs/stake_api/src/stake_state.rs b/programs/stake_api/src/stake_state.rs index 2409ca7534..dc653ccd2e 100644 --- a/programs/stake_api/src/stake_state.rs +++ b/programs/stake_api/src/stake_state.rs @@ -10,6 +10,7 @@ use solana_sdk::account_utils::State; use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; use solana_sdk::syscall; +use solana_sdk::timing::Epoch; use solana_vote_api::vote_state::VoteState; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] @@ -31,18 +32,6 @@ impl Default for StakeState { StakeState::Uninitialized } } -// TODO: trusted values of network parameters come from where? -const TICKS_PER_SECOND: f64 = 10f64; -const TICKS_PER_SLOT: f64 = 8f64; - -// credits/yr or slots/yr is seconds/year * ticks/second * slots/tick -const CREDITS_PER_YEAR: f64 = (365f64 * 24f64 * 3600f64) * TICKS_PER_SECOND / TICKS_PER_SLOT; - -// TODO: 20% is a niiice rate... TODO: make this a member of MiningPool? -const STAKE_REWARD_TARGET_RATE: f64 = 0.20; - -#[cfg(test)] -const STAKE_GETS_PAID_EVERY_VOTE: u64 = 200_000_000; // if numbers above (TICKS_YEAR) move, fix this impl StakeState { // utility function, used by Stakes, tests @@ -60,20 +49,96 @@ impl StakeState { _ => None, } } +} +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct Stake { + pub voter_pubkey: Pubkey, + pub credits_observed: u64, + pub stake: u64, // stake amount activated + pub activated: Epoch, // epoch the stake was activated + pub deactivated: Epoch, // epoch the stake was deactivated, std::Epoch::MAX if not deactivated +} +pub const STAKE_WARMUP_EPOCHS: u64 = 3; + +impl Default for Stake { + fn default() -> Self { + Stake { + voter_pubkey: Pubkey::default(), + credits_observed: 0, + stake: 0, + activated: 0, + deactivated: std::u64::MAX, + } + } +} + +impl Stake { + pub fn stake(&self, epoch: u64) -> u64 { + // before "activated" or after deactivated? + if epoch < self.activated || epoch >= self.deactivated { + return 0; + } + + // curr slot | 0 | 1 | 2 ... | 100 | 101 | 102 | 103 + // action | activate | de-activate | | + // | | | | | | | | | + // | v | | | v | | | + // stake | 1/3 | 2/3 | 3/3 ... | 3/3 | 2/3 | 1/3 | 0/3 + // ------------------------------------------------------------- + // activated | 0 ... + // deactivated | std::u64::MAX ... 103 ... + + // activate/deactivate can't possibly overlap + // (see delegate_stake() and deactivate()) + if epoch - self.activated < STAKE_WARMUP_EPOCHS { + // warmup + (self.stake / STAKE_WARMUP_EPOCHS) * (epoch - self.activated + 1) + } else if self.deactivated - epoch < STAKE_WARMUP_EPOCHS { + // cooldown + (self.stake / STAKE_WARMUP_EPOCHS) * (self.deactivated - epoch) + } else { + self.stake + } + } + + /// 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 pub fn calculate_rewards( - credits_observed: u64, - stake: u64, + &self, + point_value: f64, vote_state: &VoteState, - ) -> Option<(u64, u64)> { - if credits_observed >= vote_state.credits() { + ) -> Option<(u64, u64, u64)> { + if self.credits_observed >= vote_state.credits() { return None; } - let total_rewards = stake as f64 - * STAKE_REWARD_TARGET_RATE - * (vote_state.credits() - credits_observed) as f64 - / CREDITS_PER_YEAR; + let mut credits_observed = self.credits_observed; + let mut total_rewards = 0f64; + for (epoch, credits, prev_credits) in vote_state.epoch_credits() { + // figure out how much this stake has seen that + // for which the vote account has a record + let epoch_credits = if self.credits_observed < *prev_credits { + // the staker observed the entire epoch + credits - prev_credits + } else if self.credits_observed < *credits { + // the staker registered sometime during the epoch, partial credit + credits - credits_observed + } else { + // the staker has already observed/redeemed this epoch, or activated + // after this epoch + 0 + }; + + total_rewards += (self.stake(*epoch) * epoch_credits) as f64 * point_value; + + // don't want to assume anything about order of the iterator... + credits_observed = std::cmp::max(credits_observed, *credits); + } // don't bother trying to collect fractional lamports if total_rewards < 1f64 { @@ -87,70 +152,34 @@ impl StakeState { return None; } - Some((voter_rewards as u64, staker_rewards as u64)) - } -} - -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)] -pub struct Stake { - pub voter_pubkey: Pubkey, - pub credits_observed: u64, - pub stake: u64, // activated stake - pub epoch: u64, // epoch the stake was activated - pub prev_stake: u64, // for warmup, cooldown -} -pub const STAKE_WARMUP_EPOCHS: u64 = 3; - -impl Stake { - pub fn stake(&self, epoch: u64) -> u64 { - // prev_stake for stuff in the past - if epoch < self.epoch { - return self.prev_stake; - } - if epoch - self.epoch >= STAKE_WARMUP_EPOCHS { - return self.stake; - } - - if self.stake != 0 { - // warmup - // 1/3rd, then 2/3rds... - (self.stake / STAKE_WARMUP_EPOCHS) * (epoch - self.epoch + 1) - } else if self.prev_stake != 0 { - // cool down - // 3/3rds, then 2/3rds... - self.prev_stake - ((self.prev_stake / STAKE_WARMUP_EPOCHS) * (epoch - self.epoch)) - } else { - 0 - } + Some(( + voter_rewards as u64, + staker_rewards as u64, + credits_observed, + )) } - fn delegate( - &mut self, - stake: u64, - voter_pubkey: &Pubkey, - vote_state: &VoteState, - epoch: u64, // current: &syscall::current::Current - ) { + fn delegate(&mut self, stake: u64, voter_pubkey: &Pubkey, vote_state: &VoteState, epoch: u64) { + assert!(std::u64::MAX - epoch >= (STAKE_WARMUP_EPOCHS * 2)); + // resets the current stake's credits self.voter_pubkey = *voter_pubkey; self.credits_observed = vote_state.credits(); // when this stake was activated - self.epoch = epoch; + self.activated = epoch; self.stake = stake; } fn deactivate(&mut self, epoch: u64) { - self.voter_pubkey = Pubkey::default(); - self.credits_observed = std::u64::MAX; - self.prev_stake = self.stake(epoch); - self.stake = 0; - self.epoch = epoch; + self.deactivated = std::cmp::max( + epoch + STAKE_WARMUP_EPOCHS, + self.activated + 2 * STAKE_WARMUP_EPOCHS - 1, + ); } } pub trait StakeAccount { - fn initialize_mining_pool(&mut self) -> Result<(), InstructionError>; fn delegate_stake( &mut self, vote_account: &KeyedAccount, @@ -163,40 +192,13 @@ pub trait StakeAccount { ) -> Result<(), InstructionError>; fn redeem_vote_credits( &mut self, - stake_account: &mut KeyedAccount, vote_account: &mut KeyedAccount, + rewards_account: &mut KeyedAccount, + rewards: &syscall::rewards::Rewards, ) -> Result<(), InstructionError>; } impl<'a> StakeAccount for KeyedAccount<'a> { - fn initialize_mining_pool(&mut self) -> Result<(), InstructionError> { - if let StakeState::Uninitialized = self.state()? { - self.set_state(&StakeState::MiningPool { - epoch: 0, - point_value: 0.0, - }) - } else { - Err(InstructionError::InvalidAccountData) - } - } - - fn deactivate_stake( - &mut self, - current: &syscall::current::Current, - ) -> Result<(), InstructionError> { - if self.signer_key().is_none() { - return Err(InstructionError::MissingRequiredSignature); - } - - if let StakeState::Stake(mut stake) = self.state()? { - stake.deactivate(current.epoch); - - self.set_state(&StakeState::Stake(stake)) - } else { - Err(InstructionError::InvalidAccountData) - } - } - fn delegate_stake( &mut self, vote_account: &KeyedAccount, @@ -226,14 +228,30 @@ impl<'a> StakeAccount for KeyedAccount<'a> { Err(InstructionError::InvalidAccountData) } } + fn deactivate_stake( + &mut self, + current: &syscall::current::Current, + ) -> Result<(), InstructionError> { + if self.signer_key().is_none() { + return Err(InstructionError::MissingRequiredSignature); + } + if let StakeState::Stake(mut stake) = self.state()? { + stake.deactivate(current.epoch); + + self.set_state(&StakeState::Stake(stake)) + } else { + Err(InstructionError::InvalidAccountData) + } + } fn redeem_vote_credits( &mut self, - stake_account: &mut KeyedAccount, vote_account: &mut KeyedAccount, + rewards_account: &mut KeyedAccount, + rewards: &syscall::rewards::Rewards, ) -> Result<(), InstructionError> { - if let (StakeState::MiningPool { .. }, StakeState::Stake(mut stake)) = - (self.state()?, stake_account.state()?) + if let (StakeState::Stake(mut stake), StakeState::RewardsPool) = + (self.state()?, rewards_account.state()?) { let vote_state: VoteState = vote_account.state()?; @@ -241,25 +259,20 @@ impl<'a> StakeAccount for KeyedAccount<'a> { return Err(InstructionError::InvalidArgument); } - if stake.credits_observed > vote_state.credits() { - return Err(InstructionError::InvalidAccountData); - } - - if let Some((stakers_reward, voters_reward)) = StakeState::calculate_rewards( - stake.credits_observed, - stake_account.account.lamports, - &vote_state, - ) { - if self.account.lamports < (stakers_reward + voters_reward) { + if let Some((stakers_reward, voters_reward, credits_observed)) = + stake.calculate_rewards(rewards.validator_point_value, &vote_state) + { + if rewards_account.account.lamports < (stakers_reward + voters_reward) { return Err(InstructionError::UnbalancedInstruction); } - self.account.lamports -= stakers_reward + voters_reward; - stake_account.account.lamports += stakers_reward; + rewards_account.account.lamports -= stakers_reward + voters_reward; + + self.account.lamports += stakers_reward; vote_account.account.lamports += voters_reward; - stake.credits_observed = vote_state.credits(); + stake.credits_observed = credits_observed; - stake_account.set_state(&StakeState::Stake(stake)) + self.set_state(&StakeState::Stake(stake)) } else { // not worth collecting Err(InstructionError::CustomError(1)) @@ -283,8 +296,8 @@ pub fn create_stake_account( voter_pubkey: *voter_pubkey, credits_observed: vote_state.credits(), stake: lamports, - epoch: 0, - prev_stake: 0, + activated: 0, + deactivated: std::u64::MAX, })) .expect("set_state"); @@ -292,14 +305,8 @@ pub fn create_stake_account( } // utility function, used by Bank, tests, genesis -pub fn create_mining_pool(lamports: u64, epoch: u64, point_value: f64) -> Account { - let mut mining_pool_account = Account::new(lamports, std::mem::size_of::(), &id()); - - mining_pool_account - .set_state(&StakeState::MiningPool { epoch, point_value }) - .expect("set_state"); - - mining_pool_account +pub fn create_rewards_pool() -> Account { + Account::new_data(std::u64::MAX, &StakeState::RewardsPool, &crate::id()).unwrap() } #[cfg(test)] @@ -359,8 +366,8 @@ mod tests { voter_pubkey: vote_keypair.pubkey(), credits_observed: vote_state.credits(), stake: stake_lamports, - epoch: 0, - prev_stake: 0 + activated: 0, + deactivated: std::u64::MAX, }) ); // verify that delegate_stake can't be called twice StakeState::default() @@ -414,190 +421,180 @@ mod tests { #[test] fn test_stake_state_calculate_rewards() { let mut vote_state = VoteState::default(); - let mut vote_i = 0; + let mut stake = Stake::default(); - // put a credit in the vote_state - while vote_state.credits() == 0 { - vote_state.process_slot_vote_unchecked(vote_i); - vote_i += 1; - } - // this guy can't collect now, not enough stake to get paid on 1 credit - assert_eq!(None, StakeState::calculate_rewards(0, 100, &vote_state)); - // this guy can + // warmup makes this look like zero until WARMUP_EPOCHS + stake.stake = 1; + + // this one can't collect now, credits_observed == vote_state.credits() + assert_eq!(None, stake.calculate_rewards(1_000_000_000.0, &vote_state)); + + // put 2 credits in at epoch 0 + vote_state.increment_credits(0); + vote_state.increment_credits(0); + + // this one can't collect now, no epoch credits have been saved off + assert_eq!(None, stake.calculate_rewards(1_000_000_000.0, &vote_state)); + + // put 1 credit in epoch 1, pushes the 2 above into a redeemable state + vote_state.increment_credits(1); + + // still can't collect yet, warmup puts the kibosh on it + assert_eq!(None, stake.calculate_rewards(1.0, &vote_state)); + + stake.stake = STAKE_WARMUP_EPOCHS; + // this one should be able to collect exactly 2 assert_eq!( - Some((0, 1)), - StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state) - ); - // but, there's not enough to split - vote_state.commission = std::u32::MAX / 2; - assert_eq!( - None, - StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state) + Some((0, 1 * 2, 2)), + stake.calculate_rewards(1.0, &vote_state) ); - // put more credit in the vote_state - while vote_state.credits() < 10 { - vote_state.process_slot_vote_unchecked(vote_i); - vote_i += 1; - } - vote_state.commission = 0; + stake.stake = STAKE_WARMUP_EPOCHS; + stake.credits_observed = 1; + // this one should be able to collect exactly 1 (only observed one) assert_eq!( - Some((0, 10)), - StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state) + Some((0, 1 * 1, 2)), + stake.calculate_rewards(1.0, &vote_state) ); - vote_state.commission = std::u32::MAX; + + stake.stake = STAKE_WARMUP_EPOCHS; + stake.credits_observed = 2; + // this one should be able to collect none because credits_observed >= credits in a + // redeemable state (the 2 credits in epoch 0) + assert_eq!(None, stake.calculate_rewards(1.0, &vote_state)); + + // put 1 credit in epoch 2, pushes the 1 for epoch 1 to redeemable + vote_state.increment_credits(2); + // this one should be able to collect two now, one credit by a stake of 2 assert_eq!( - Some((10, 0)), - StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state) + Some((0, 2 * 1, 3)), + stake.calculate_rewards(1.0, &vote_state) ); - vote_state.commission = std::u32::MAX / 2; + + 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((5, 5)), - StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state) + Some((0, 2 * 1 + 1 * 2, 3)), + stake.calculate_rewards(1.0, &vote_state) + ); + + // same as above, but is a really small commission out of 32 bits, + // 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)), + stake.calculate_rewards(1.0, &vote_state) + ); + vote_state.commission = std::u32::MAX - 1; + assert_eq!( + None, // would be pSome((0, 2 * 1 + 1 * 2, 3)), + stake.calculate_rewards(1.0, &vote_state) ); - // not even enough stake to get paid on 10 credits... - assert_eq!(None, StakeState::calculate_rewards(0, 100, &vote_state)); } #[test] fn test_stake_redeem_vote_credits() { let current = syscall::current::Current::default(); + let mut rewards = syscall::rewards::Rewards::default(); + rewards.validator_point_value = 100.0; - let vote_keypair = Keypair::new(); - let mut vote_state = VoteState::default(); - for i in 0..1000 { - vote_state.process_slot_vote_unchecked(i); - } - - let vote_pubkey = vote_keypair.pubkey(); - let mut vote_account = - vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100); - let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account); - vote_keyed_account.set_state(&vote_state).unwrap(); + let rewards_pool_pubkey = Pubkey::new_rand(); + let mut rewards_pool_account = create_rewards_pool(); + let mut rewards_pool_keyed_account = + KeyedAccount::new(&rewards_pool_pubkey, false, &mut rewards_pool_account); let pubkey = Pubkey::default(); - let mut stake_account = Account::new( - STAKE_GETS_PAID_EVERY_VOTE, - std::mem::size_of::(), - &id(), - ); - let mut stake_keyed_account = KeyedAccount::new(&pubkey, true, &mut stake_account); - - // delegate the stake - assert!(stake_keyed_account - .delegate_stake(&vote_keyed_account, STAKE_GETS_PAID_EVERY_VOTE, ¤t) - .is_ok()); - - let mut mining_pool_account = Account::new(0, std::mem::size_of::(), &id()); - let mut mining_pool_keyed_account = - KeyedAccount::new(&pubkey, true, &mut mining_pool_account); - - // not a mining pool yet... - assert_eq!( - mining_pool_keyed_account - .redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account), - Err(InstructionError::InvalidAccountData) - ); - - mining_pool_keyed_account - .set_state(&StakeState::MiningPool { - epoch: 0, - point_value: 0.0, - }) - .unwrap(); - - // no movement in vote account, so no redemption needed - assert_eq!( - mining_pool_keyed_account - .redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account), - Err(InstructionError::CustomError(1)) - ); - - // move the vote account forward - vote_state.process_slot_vote_unchecked(1000); - vote_keyed_account.set_state(&vote_state).unwrap(); - - // now, no lamports in the pool! - assert_eq!( - mining_pool_keyed_account - .redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account), - Err(InstructionError::UnbalancedInstruction) - ); - - // add a lamport to pool - mining_pool_keyed_account.account.lamports = 2; - assert!(mining_pool_keyed_account - .redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account) - .is_ok()); // yay - - // lamports only shifted around, none made or lost - assert_eq!( - 2 + 100 + STAKE_GETS_PAID_EVERY_VOTE, - mining_pool_account.lamports + vote_account.lamports + stake_account.lamports - ); - } - - #[test] - fn test_stake_redeem_vote_credits_vote_errors() { - let current = syscall::current::Current::default(); - - let vote_keypair = Keypair::new(); - let mut vote_state = VoteState::default(); - for i in 0..1000 { - vote_state.process_slot_vote_unchecked(i); - } - - let vote_pubkey = vote_keypair.pubkey(); - let mut vote_account = - vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100); - let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account); - vote_keyed_account.set_state(&vote_state).unwrap(); - - let pubkey = Pubkey::default(); - let stake_lamports = 0; + let stake_lamports = 100; let mut stake_account = Account::new(stake_lamports, std::mem::size_of::(), &id()); let mut stake_keyed_account = KeyedAccount::new(&pubkey, true, &mut stake_account); + let vote_pubkey = Pubkey::new_rand(); + let mut vote_account = + vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100); + let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account); + + // not delegated yet, deserialization fails + assert_eq!( + stake_keyed_account.redeem_vote_credits( + &mut vote_keyed_account, + &mut rewards_pool_keyed_account, + &rewards + ), + Err(InstructionError::InvalidAccountData) + ); + // delegate the stake assert!(stake_keyed_account .delegate_stake(&vote_keyed_account, stake_lamports, ¤t) .is_ok()); - - let mut mining_pool_account = Account::new(0, std::mem::size_of::(), &id()); - let mut mining_pool_keyed_account = - KeyedAccount::new(&pubkey, true, &mut mining_pool_account); - mining_pool_keyed_account - .set_state(&StakeState::MiningPool { - epoch: 0, - point_value: 0.0, - }) - .unwrap(); - - let mut vote_state = VoteState::default(); - for i in 0..100 { - // go back in time, previous state had 1000 votes - vote_state.process_slot_vote_unchecked(i); - } - vote_keyed_account.set_state(&vote_state).unwrap(); - // voter credits lower than stake_delegate credits... TODO: is this an error? + // no credits to claim assert_eq!( - mining_pool_keyed_account - .redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account), + stake_keyed_account.redeem_vote_credits( + &mut vote_keyed_account, + &mut rewards_pool_keyed_account, + &rewards + ), + Err(InstructionError::CustomError(1)) + ); + + // swapped rewards and vote, deserialization of rewards_pool fails + assert_eq!( + stake_keyed_account.redeem_vote_credits( + &mut rewards_pool_keyed_account, + &mut vote_keyed_account, + &rewards + ), Err(InstructionError::InvalidAccountData) ); - let vote1_keypair = Keypair::new(); - let vote1_pubkey = vote1_keypair.pubkey(); - let mut vote1_account = - vote_state::create_account(&vote1_pubkey, &Pubkey::new_rand(), 0, 100); - let mut vote1_keyed_account = KeyedAccount::new(&vote1_pubkey, false, &mut vote1_account); - vote1_keyed_account.set_state(&vote_state).unwrap(); + let mut vote_account = + vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100); + + let mut vote_state = VoteState::from(&vote_account).unwrap(); + // put in some credits in epoch 0 for which we should have a non-zero stake + for _i in 0..100 { + vote_state.increment_credits(0); + } + vote_state.increment_credits(1); + + vote_state.to(&mut vote_account).unwrap(); + let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account); + + // some credits to claim, but rewards pool empty (shouldn't ever happen) + rewards_pool_keyed_account.account.lamports = 1; + assert_eq!( + stake_keyed_account.redeem_vote_credits( + &mut vote_keyed_account, + &mut rewards_pool_keyed_account, + &rewards + ), + Err(InstructionError::UnbalancedInstruction) + ); + rewards_pool_keyed_account.account.lamports = std::u64::MAX; + + // finally! some credits to claim + assert_eq!( + stake_keyed_account.redeem_vote_credits( + &mut vote_keyed_account, + &mut rewards_pool_keyed_account, + &rewards + ), + Ok(()) + ); + + let wrong_vote_pubkey = Pubkey::new_rand(); + let mut wrong_vote_keyed_account = + KeyedAccount::new(&wrong_vote_pubkey, false, &mut vote_account); // wrong voter_pubkey... assert_eq!( - mining_pool_keyed_account - .redeem_vote_credits(&mut stake_keyed_account, &mut vote1_keyed_account), + stake_keyed_account.redeem_vote_credits( + &mut wrong_vote_keyed_account, + &mut rewards_pool_keyed_account, + &rewards + ), Err(InstructionError::InvalidArgument) ); } diff --git a/programs/vote_api/src/vote_instruction.rs b/programs/vote_api/src/vote_instruction.rs index d8bd5f2654..b98aeebaf2 100644 --- a/programs/vote_api/src/vote_instruction.rs +++ b/programs/vote_api/src/vote_instruction.rs @@ -10,7 +10,7 @@ use solana_metrics::datapoint_warn; use solana_sdk::account::KeyedAccount; use solana_sdk::instruction::{AccountMeta, Instruction, InstructionError}; use solana_sdk::pubkey::Pubkey; -use solana_sdk::syscall::slot_hashes; +use solana_sdk::syscall; use solana_sdk::system_instruction; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] @@ -52,15 +52,22 @@ pub fn create_account( fn metas_for_authorized_signer( vote_pubkey: &Pubkey, authorized_voter_pubkey: &Pubkey, // currently authorized + other_params: &[AccountMeta], ) -> Vec { let is_own_signer = authorized_voter_pubkey == vote_pubkey; // vote account let mut account_metas = vec![AccountMeta::new(*vote_pubkey, is_own_signer)]; + for meta in other_params { + account_metas.push(meta.clone()); + } + + // append signer at the end if !is_own_signer { account_metas.push(AccountMeta::new(*authorized_voter_pubkey, true)) // signer } + account_metas } @@ -69,7 +76,7 @@ pub fn authorize_voter( authorized_voter_pubkey: &Pubkey, // currently authorized new_authorized_voter_pubkey: &Pubkey, ) -> Instruction { - let account_metas = metas_for_authorized_signer(vote_pubkey, authorized_voter_pubkey); + let account_metas = metas_for_authorized_signer(vote_pubkey, authorized_voter_pubkey, &[]); Instruction::new( id(), @@ -83,10 +90,16 @@ pub fn vote( authorized_voter_pubkey: &Pubkey, recent_votes: Vec, ) -> Instruction { - let mut account_metas = metas_for_authorized_signer(vote_pubkey, authorized_voter_pubkey); - - // request slot_hashes syscall account after vote_pubkey - account_metas.insert(1, AccountMeta::new(slot_hashes::id(), false)); + let account_metas = metas_for_authorized_signer( + vote_pubkey, + authorized_voter_pubkey, + &[ + // request slot_hashes syscall account after vote_pubkey + AccountMeta::new(syscall::slot_hashes::id(), false), + // request current syscall account after that + AccountMeta::new(syscall::current::id(), false), + ], + ); Instruction::new(id(), &VoteInstruction::Vote(recent_votes), account_metas) } @@ -119,9 +132,18 @@ pub fn process_instruction( } VoteInstruction::Vote(votes) => { datapoint_warn!("vote-native", ("count", 1, i64)); - let (slot_hashes, other_signers) = rest.split_at_mut(1); - let slot_hashes = &mut slot_hashes[0]; - vote_state::process_votes(me, slot_hashes, other_signers, &votes) + if rest.len() < 2 { + Err(InstructionError::InvalidInstructionData)?; + } + let (slot_hashes_and_current, other_signers) = rest.split_at_mut(2); + + vote_state::process_votes( + me, + &syscall::slot_hashes::from_keyed_account(&slot_hashes_and_current[0])?, + &syscall::current::from_keyed_account(&slot_hashes_and_current[1])?, + other_signers, + &votes, + ) } } } @@ -141,7 +163,20 @@ mod tests { } fn process_instruction(instruction: &Instruction) -> Result<(), InstructionError> { - let mut accounts = vec![]; + let mut accounts: Vec<_> = instruction + .accounts + .iter() + .map(|meta| { + if syscall::current::check_id(&meta.pubkey) { + syscall::current::create_account(1, 0, 0, 0) + } else if syscall::slot_hashes::check_id(&meta.pubkey) { + syscall::slot_hashes::create_account(1, &[]) + } else { + Account::default() + } + }) + .collect(); + for _ in 0..instruction.accounts.len() { accounts.push(Account::default()); } diff --git a/programs/vote_api/src/vote_state.rs b/programs/vote_api/src/vote_state.rs index e208a5b5f6..a47dc1b964 100644 --- a/programs/vote_api/src/vote_state.rs +++ b/programs/vote_api/src/vote_state.rs @@ -9,30 +9,35 @@ use solana_sdk::account_utils::State; use solana_sdk::hash::Hash; use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; -use solana_sdk::syscall::slot_hashes; +use solana_sdk::syscall::current::Current; +pub use solana_sdk::timing::{Epoch, Slot}; use std::collections::VecDeque; // Maximum number of votes to keep around pub const MAX_LOCKOUT_HISTORY: usize = 31; pub const INITIAL_LOCKOUT: usize = 2; -#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)] +// Maximum number of credits history to keep around +// smaller numbers makes +pub const MAX_EPOCH_CREDITS_HISTORY: usize = 64; + +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub struct Vote { /// A vote for height slot - pub slot: u64, + pub slot: Slot, // signature of the bank's state at given slot pub hash: Hash, } impl Vote { - pub fn new(slot: u64, hash: Hash) -> Self { + pub fn new(slot: Slot, hash: Hash) -> Self { Self { slot, hash } } } #[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct Lockout { - pub slot: u64, + pub slot: Slot, pub confirmation_count: u32, } @@ -51,10 +56,10 @@ impl Lockout { // The slot height at which this vote expires (cannot vote for any slot // less than this) - pub fn expiration_slot(&self) -> u64 { + pub fn expiration_slot(&self) -> Slot { self.slot + self.lockout() } - pub fn is_expired(&self, slot: u64) -> bool { + pub fn is_expired(&self, slot: Slot) -> bool { self.expiration_slot() < slot } } @@ -68,21 +73,27 @@ pub struct VoteState { /// payout should be given to this VoteAccount pub commission: u32, pub root_slot: Option, + + /// current epoch + epoch: Epoch, + /// current 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)>, } impl VoteState { pub fn new(vote_pubkey: &Pubkey, node_pubkey: &Pubkey, commission: u32) -> Self { - let votes = VecDeque::new(); - let credits = 0; - let root_slot = None; Self { - votes, node_pubkey: *node_pubkey, authorized_voter_pubkey: *vote_pubkey, - credits, commission, - root_slot, + ..VoteState::default() } } @@ -92,6 +103,7 @@ impl VoteState { let mut vote_state = Self::default(); vote_state.votes = VecDeque::from(vec![Lockout::default(); MAX_LOCKOUT_HISTORY]); vote_state.root_slot = Some(std::u64::MAX); + vote_state.epoch_credits = vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY]; serialized_size(&vote_state).unwrap() as usize } @@ -136,11 +148,13 @@ impl VoteState { } } - pub fn process_votes(&mut self, votes: &[Vote], slot_hashes: &[(u64, Hash)]) { - votes.iter().for_each(|v| self.process_vote(v, slot_hashes)); + pub fn process_votes(&mut self, votes: &[Vote], slot_hashes: &[(Slot, Hash)], epoch: Epoch) { + votes + .iter() + .for_each(|v| self.process_vote(v, slot_hashes, epoch)); } - pub fn process_vote(&mut self, vote: &Vote, slot_hashes: &[(u64, Hash)]) { + pub fn process_vote(&mut self, vote: &Vote, slot_hashes: &[(Slot, Hash)], epoch: Epoch) { // Ignore votes for slots earlier than we already have votes for if self .votes @@ -176,24 +190,45 @@ impl VoteState { let vote = Lockout::new(&vote); - // TODO: Integrity checks - // Verify the vote's bank hash matches what is expected - self.pop_expired_votes(vote.slot); + // Once the stack is full, pop the oldest vote and distribute rewards if self.votes.len() == MAX_LOCKOUT_HISTORY { let vote = self.votes.pop_front().unwrap(); self.root_slot = Some(vote.slot); - self.credits += 1; + + self.increment_credits(epoch); } self.votes.push_back(vote); self.double_lockouts(); } - pub fn process_vote_unchecked(&mut self, vote: &Vote) { - self.process_vote(vote, &[(vote.slot, vote.hash)]); + /// increment credits, record credits for last epoch if new epoch + pub fn increment_credits(&mut self, epoch: Epoch) { + // record credits by epoch + + if epoch != self.epoch { + // encode the delta, but be able to return partial for stakers who + // attach halfway through an epoch + self.epoch_credits + .push((self.epoch, self.credits, self.last_epoch_credits)); + // 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; } - pub fn process_slot_vote_unchecked(&mut self, slot: u64) { + + /// "uncheckeds" used by tests, locktower + pub fn process_vote_unchecked(&mut self, vote: &Vote) { + self.process_vote(vote, &[(vote.slot, vote.hash)], self.epoch); + } + pub fn process_slot_vote_unchecked(&mut self, slot: Slot) { self.process_vote_unchecked(&Vote::new(slot, Hash::default())); } @@ -212,6 +247,13 @@ impl VoteState { self.credits } + /// Number of "credits" owed to this account from the mining pool on a per-epoch basis, + /// starting from credits observed. + /// Each tuple of (Epoch, u64) is the credits() delta as of the end of the Epoch + pub fn epoch_credits(&self) -> impl Iterator { + self.epoch_credits.iter() + } + fn pop_expired_votes(&mut self, slot: u64) { loop { if self.votes.back().map_or(false, |v| v.is_expired(slot)) { @@ -280,7 +322,8 @@ pub fn initialize_account( pub fn process_votes( vote_account: &mut KeyedAccount, - slot_hashes_account: &mut KeyedAccount, + slot_hashes: &[(Slot, Hash)], + current: &Current, other_signers: &[KeyedAccount], votes: &[Vote], ) -> Result<(), InstructionError> { @@ -290,12 +333,6 @@ pub fn process_votes( return Err(InstructionError::UninitializedAccount); } - if !slot_hashes::check_id(slot_hashes_account.unsigned_key()) { - return Err(InstructionError::InvalidArgument); - } - - let slot_hashes: Vec<(u64, Hash)> = slot_hashes_account.state()?; - let authorized = Some(&vote_state.authorized_voter_pubkey); // find a signer that matches the authorized_voter_pubkey if vote_account.signer_key() != authorized @@ -306,7 +343,7 @@ pub fn process_votes( return Err(InstructionError::MissingRequiredSignature); } - vote_state.process_votes(&votes, &slot_hashes); + vote_state.process_votes(&votes, slot_hashes, current.epoch); vote_account.set_state(&vote_state) } @@ -319,12 +356,10 @@ pub fn create_account( ) -> Account { let mut vote_account = Account::new(lamports, VoteState::size_of(), &id()); - initialize_account( - &mut KeyedAccount::new(vote_pubkey, false, &mut vote_account), - node_pubkey, - commission, - ) - .unwrap(); + VoteState::new(vote_pubkey, node_pubkey, commission) + .to(&mut vote_account) + .unwrap(); + vote_account } @@ -351,12 +386,9 @@ pub fn create_bootstrap_leader_account( mod tests { use super::*; use crate::vote_state; - use bincode::serialized_size; use solana_sdk::account::Account; use solana_sdk::account_utils::State; use solana_sdk::hash::hash; - use solana_sdk::syscall; - use solana_sdk::syscall::slot_hashes; const MAX_RECENT_VOTES: usize = 16; @@ -385,30 +417,20 @@ mod tests { ) } - fn create_test_slot_hashes_account(slot_hashes: &[(u64, Hash)]) -> (Pubkey, Account) { - let mut slot_hashes_account = Account::new( - 0, - serialized_size(&slot_hashes).unwrap() as usize, - &syscall::id(), - ); - slot_hashes_account - .set_state(&slot_hashes.to_vec()) - .unwrap(); - (slot_hashes::id(), slot_hashes_account) - } - fn simulate_process_vote( vote_pubkey: &Pubkey, vote_account: &mut Account, vote: &Vote, slot_hashes: &[(u64, Hash)], + epoch: u64, ) -> Result { - let (slot_hashes_id, mut slot_hashes_account) = - create_test_slot_hashes_account(slot_hashes); - process_votes( &mut KeyedAccount::new(vote_pubkey, true, vote_account), - &mut KeyedAccount::new(&slot_hashes_id, false, &mut slot_hashes_account), + slot_hashes, + &Current { + epoch, + ..Current::default() + }, &[], &[vote.clone()], )?; @@ -421,7 +443,13 @@ mod tests { vote_account: &mut Account, vote: &Vote, ) -> Result { - simulate_process_vote(vote_pubkey, vote_account, vote, &[(vote.slot, vote.hash)]) + simulate_process_vote( + vote_pubkey, + vote_account, + vote, + &[(vote.slot, vote.hash)], + 0, + ) } #[test] @@ -479,58 +507,45 @@ mod tests { &mut vote_account, &vote, &[(0, Hash::default())], + 0, ) .unwrap(); assert_eq!(vote_state.votes.len(), 0); // wrong slot let vote_state = - simulate_process_vote(&vote_pubkey, &mut vote_account, &vote, &[(1, hash)]).unwrap(); + simulate_process_vote(&vote_pubkey, &mut vote_account, &vote, &[(1, hash)], 0).unwrap(); assert_eq!(vote_state.votes.len(), 0); // empty slot_hashes let vote_state = - simulate_process_vote(&vote_pubkey, &mut vote_account, &vote, &[]).unwrap(); + simulate_process_vote(&vote_pubkey, &mut vote_account, &vote, &[], 0).unwrap(); assert_eq!(vote_state.votes.len(), 0); - - // this one would work, but the wrong account is passed for slot_hashes_id - let (_slot_hashes_id, mut slot_hashes_account) = - create_test_slot_hashes_account(&[(vote.slot, vote.hash)]); - assert_eq!( - process_votes( - &mut KeyedAccount::new(&vote_pubkey, true, &mut vote_account), - &mut KeyedAccount::new(&Pubkey::default(), false, &mut slot_hashes_account), - &[], - &[vote.clone()], - ), - Err(InstructionError::InvalidArgument) - ); } #[test] fn test_vote_signature() { let (vote_pubkey, mut vote_account) = create_test_account(); - let vote = vec![Vote::new(1, Hash::default())]; - - let (slot_hashes_id, mut slot_hashes_account) = - create_test_slot_hashes_account(&[(1, Hash::default())]); + let vote = Vote::new(1, Hash::default()); // unsigned let res = process_votes( &mut KeyedAccount::new(&vote_pubkey, false, &mut vote_account), - &mut KeyedAccount::new(&slot_hashes_id, false, &mut slot_hashes_account), + &[(vote.slot, vote.hash)], + &Current::default(), &[], - &vote, + &[vote], ); assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); // unsigned let res = process_votes( &mut KeyedAccount::new(&vote_pubkey, true, &mut vote_account), - &mut KeyedAccount::new(&slot_hashes_id, false, &mut slot_hashes_account), + &[(vote.slot, vote.hash)], + &Current::default(), &[], - &vote, + &[vote], ); assert_eq!(res, Ok(())); @@ -562,28 +577,28 @@ mod tests { assert_eq!(res, Ok(())); // not signed by authorized voter - let vote = vec![Vote::new(2, Hash::default())]; - let (slot_hashes_id, mut slot_hashes_account) = - create_test_slot_hashes_account(&[(2, Hash::default())]); + let vote = Vote::new(2, Hash::default()); let res = process_votes( &mut KeyedAccount::new(&vote_pubkey, true, &mut vote_account), - &mut KeyedAccount::new(&slot_hashes_id, false, &mut slot_hashes_account), + &[(vote.slot, vote.hash)], + &Current::default(), &[], - &vote, + &[vote], ); assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); // signed by authorized voter - let vote = vec![Vote::new(2, Hash::default())]; + let vote = Vote::new(2, Hash::default()); let res = process_votes( &mut KeyedAccount::new(&vote_pubkey, false, &mut vote_account), - &mut KeyedAccount::new(&slot_hashes_id, false, &mut slot_hashes_account), + &[(vote.slot, vote.hash)], + &Current::default(), &[KeyedAccount::new( &authorized_voter_pubkey, true, &mut Account::default(), )], - &vote, + &[vote], ); assert_eq!(res, Ok(())); } @@ -773,8 +788,8 @@ mod tests { .collect(); let slot_hashes: Vec<_> = votes.iter().map(|vote| (vote.slot, vote.hash)).collect(); - vote_state_a.process_votes(&votes, &slot_hashes); - vote_state_b.process_votes(&votes, &slot_hashes); + vote_state_a.process_votes(&votes, &slot_hashes, 0); + vote_state_b.process_votes(&votes, &slot_hashes, 0); assert_eq!(recent_votes(&vote_state_a), recent_votes(&vote_state_b)); } @@ -796,4 +811,42 @@ mod tests { ); } + #[test] + fn test_vote_state_epoch_credits() { + let mut vote_state = VoteState::default(); + + assert_eq!(vote_state.credits(), 0); + assert_eq!( + vote_state + .epoch_credits() + .cloned() + .collect::>(), + vec![] + ); + + let mut expected = vec![]; + let mut credits = 0; + let epochs = (MAX_EPOCH_CREDITS_HISTORY + 2) as u64; + for epoch in 0..epochs { + for _j in 0..epoch { + vote_state.increment_credits(epoch); + credits += 1; + } + 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); + } + + assert_eq!(vote_state.credits(), credits); + assert_eq!( + vote_state + .epoch_credits() + .cloned() + .collect::>(), + expected + ); + } + } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 8ab2b08bd0..8c473556a6 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -417,7 +417,7 @@ impl Bank { fn update_slot_hashes(&self) { let mut account = self .get_account(&slot_hashes::id()) - .unwrap_or_else(|| slot_hashes::create_account(1)); + .unwrap_or_else(|| slot_hashes::create_account(1, &[])); let mut slot_hashes = SlotHashes::from(&account).unwrap(); slot_hashes.add(self.slot(), self.hash()); @@ -546,6 +546,10 @@ impl Bank { self.capitalization .fetch_add(account.lamports as usize, Ordering::Relaxed); } + for (pubkey, account) in genesis_block.rewards_pools.iter() { + self.store_account(pubkey, account); + } + // highest staked node is the first collector self.collector_id = self .stakes diff --git a/sdk/src/syscall/current.rs b/sdk/src/syscall/current.rs index 7bb8dae111..c5c079815f 100644 --- a/sdk/src/syscall/current.rs +++ b/sdk/src/syscall/current.rs @@ -4,6 +4,8 @@ use crate::account::Account; use crate::syscall; use bincode::serialized_size; +pub use crate::timing::{Epoch, Slot}; + crate::solana_name_id!(ID, "Sysca11Current11111111111111111111111111111"); const ID: [u8; 32] = [ @@ -14,9 +16,9 @@ const ID: [u8; 32] = [ #[repr(C)] #[derive(Serialize, Deserialize, Debug, Default, PartialEq)] pub struct Current { - pub slot: u64, - pub epoch: u64, - pub stakers_epoch: u64, + pub slot: Slot, + pub epoch: Epoch, + pub stakers_epoch: Epoch, } impl Current { @@ -32,7 +34,7 @@ impl Current { } } -pub fn create_account(lamports: u64, slot: u64, epoch: u64, stakers_epoch: u64) -> Account { +pub fn create_account(lamports: u64, slot: Slot, epoch: Epoch, stakers_epoch: Epoch) -> Account { Account::new_data( lamports, &Current { @@ -45,6 +47,15 @@ pub fn create_account(lamports: u64, slot: u64, epoch: u64, stakers_epoch: u64) .unwrap() } +use crate::account::KeyedAccount; +use crate::instruction::InstructionError; +pub fn from_keyed_account(account: &KeyedAccount) -> Result { + if !check_id(account.unsigned_key()) { + return Err(InstructionError::InvalidArgument); + } + Current::from(account.account).ok_or(InstructionError::InvalidArgument) +} + #[cfg(test)] mod tests { use super::*; diff --git a/sdk/src/syscall/rewards.rs b/sdk/src/syscall/rewards.rs index b13097943a..8aaf393c3f 100644 --- a/sdk/src/syscall/rewards.rs +++ b/sdk/src/syscall/rewards.rs @@ -47,6 +47,16 @@ pub fn create_account( .unwrap() } +use crate::account::KeyedAccount; +use crate::instruction::InstructionError; +pub fn from_keyed_account(account: &KeyedAccount) -> Result { + if !check_id(account.unsigned_key()) { + dbg!(account.unsigned_key()); + return Err(InstructionError::InvalidArgument); + } + Rewards::from(account.account).ok_or(InstructionError::InvalidAccountData) +} + #[cfg(test)] mod tests { use super::*; diff --git a/sdk/src/syscall/slot_hashes.rs b/sdk/src/syscall/slot_hashes.rs index 42e2a6f4f0..cce9fb24db 100644 --- a/sdk/src/syscall/slot_hashes.rs +++ b/sdk/src/syscall/slot_hashes.rs @@ -9,6 +9,8 @@ use crate::syscall; use bincode::serialized_size; use std::ops::Deref; +pub use crate::timing::Slot; + /// "Sysca11SlotHashes11111111111111111111111111" /// slot hashes account pubkey const ID: [u8; 32] = [ @@ -29,7 +31,7 @@ pub const MAX_SLOT_HASHES: usize = 512; // 512 slots to get your vote in #[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct SlotHashes { // non-pub to keep control of size - inner: Vec<(u64, Hash)>, + inner: Vec<(Slot, Hash)>, } impl SlotHashes { @@ -46,10 +48,15 @@ impl SlotHashes { }) .unwrap() as usize } - pub fn add(&mut self, slot: u64, hash: Hash) { + pub fn add(&mut self, slot: Slot, hash: Hash) { self.inner.insert(0, (slot, hash)); self.inner.truncate(MAX_SLOT_HASHES); } + pub fn new(slot_hashes: &[(Slot, Hash)]) -> Self { + Self { + inner: slot_hashes.to_vec(), + } + } } impl Deref for SlotHashes { @@ -59,8 +66,19 @@ impl Deref for SlotHashes { } } -pub fn create_account(lamports: u64) -> Account { - Account::new(lamports, SlotHashes::size_of(), &syscall::id()) +pub fn create_account(lamports: u64, slot_hashes: &[(Slot, Hash)]) -> Account { + let mut account = Account::new(lamports, SlotHashes::size_of(), &syscall::id()); + SlotHashes::new(slot_hashes).to(&mut account).unwrap(); + account +} + +use crate::account::KeyedAccount; +use crate::instruction::InstructionError; +pub fn from_keyed_account(account: &KeyedAccount) -> Result { + if !check_id(account.unsigned_key()) { + return Err(InstructionError::InvalidArgument); + } + SlotHashes::from(account.account).ok_or(InstructionError::InvalidArgument) } #[cfg(test)] @@ -71,7 +89,8 @@ mod tests { #[test] fn test_create_account() { let lamports = 42; - let account = create_account(lamports); + let account = create_account(lamports, &[]); + assert_eq!(account.data.len(), SlotHashes::size_of()); let slot_hashes = SlotHashes::from(&account); assert_eq!(slot_hashes, Some(SlotHashes { inner: vec![] })); let mut slot_hashes = slot_hashes.unwrap(); diff --git a/sdk/src/timing.rs b/sdk/src/timing.rs index 8f799aff17..19dfd2f344 100644 --- a/sdk/src/timing.rs +++ b/sdk/src/timing.rs @@ -57,3 +57,11 @@ pub fn timestamp() -> u64 { .expect("create timestamp in timing"); duration_as_ms(&now) } + +/// Slot is a unit of time given to a leader for encoding, +/// is some some number of Ticks long. Use a u64 to count them. +pub type Slot = u64; + +/// Epoch is a unit of time a given leader schedule is honored, +/// some number of Slots. Use a u64 to count them. +pub type Epoch = u64; diff --git a/wallet/src/wallet.rs b/wallet/src/wallet.rs index 281f361f3f..593dc29a37 100644 --- a/wallet/src/wallet.rs +++ b/wallet/src/wallet.rs @@ -51,9 +51,8 @@ pub enum WalletCommand { CreateVoteAccount(Pubkey, Pubkey, u32, u64), ShowVoteAccount(Pubkey), CreateStakeAccount(Pubkey, u64), - CreateMiningPoolAccount(Pubkey, u64), DelegateStake(Keypair, Pubkey, u64), - RedeemVoteCredits(Pubkey, Pubkey, Pubkey), + RedeemVoteCredits(Pubkey, Pubkey), ShowStakeAccount(Pubkey), CreateStorageMiningPoolAccount(Pubkey, u64), CreateReplicatorStorageAccount(Pubkey, Pubkey), @@ -238,15 +237,6 @@ pub fn parse_command( lamports, )) } - ("create-mining-pool-account", Some(matches)) => { - let mining_pool_account_pubkey = - value_of(matches, "mining_pool_account_pubkey").unwrap(); - let lamports = matches.value_of("lamports").unwrap().parse()?; - Ok(WalletCommand::CreateMiningPoolAccount( - mining_pool_account_pubkey, - lamports, - )) - } ("delegate-stake", Some(matches)) => { let staking_account_keypair = keypair_of(matches, "staking_account_keypair_file").unwrap(); @@ -259,12 +249,9 @@ pub fn parse_command( )) } ("redeem-vote-credits", Some(matches)) => { - let mining_pool_account_pubkey = - value_of(matches, "mining_pool_account_pubkey").unwrap(); let staking_account_pubkey = value_of(matches, "staking_account_pubkey").unwrap(); let voting_account_pubkey = value_of(matches, "voting_account_pubkey").unwrap(); Ok(WalletCommand::RedeemVoteCredits( - mining_pool_account_pubkey, staking_account_pubkey, voting_account_pubkey, )) @@ -273,15 +260,6 @@ pub fn parse_command( let staking_account_pubkey = value_of(matches, "staking_account_pubkey").unwrap(); Ok(WalletCommand::ShowStakeAccount(staking_account_pubkey)) } - ("create-storage-mining-pool-account", Some(matches)) => { - let storage_mining_pool_account_pubkey = - value_of(matches, "storage_mining_pool_account_pubkey").unwrap(); - let lamports = matches.value_of("lamports").unwrap().parse()?; - Ok(WalletCommand::CreateStorageMiningPoolAccount( - storage_mining_pool_account_pubkey, - lamports, - )) - } ("create-replicator-storage-account", Some(matches)) => { let account_owner = value_of(matches, "storage_account_owner").unwrap(); let storage_account_pubkey = value_of(matches, "storage_account_pubkey").unwrap(); @@ -580,28 +558,6 @@ fn process_create_stake_account( Ok(signature_str.to_string()) } -fn process_create_mining_pool_account( - rpc_client: &RpcClient, - config: &WalletConfig, - mining_pool_account_pubkey: &Pubkey, - lamports: u64, -) -> ProcessResult { - let (recent_blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?; - let ixs = stake_instruction::create_mining_pool_account( - &config.keypair.pubkey(), - mining_pool_account_pubkey, - lamports, - ); - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair], - recent_blockhash, - ); - let signature_str = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair])?; - Ok(signature_str.to_string()) -} - fn process_delegate_stake( rpc_client: &RpcClient, config: &WalletConfig, @@ -631,13 +587,11 @@ fn process_delegate_stake( fn process_redeem_vote_credits( rpc_client: &RpcClient, config: &WalletConfig, - mining_pool_account_pubkey: &Pubkey, staking_account_pubkey: &Pubkey, voting_account_pubkey: &Pubkey, ) -> ProcessResult { let (recent_blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?; let ixs = vec![stake_instruction::redeem_vote_credits( - mining_pool_account_pubkey, staking_account_pubkey, voting_account_pubkey, )]; @@ -663,15 +617,7 @@ fn process_show_stake_account( println!("account lamports: {}", stake_account.lamports); println!("voter pubkey: {}", stake.voter_pubkey); println!("credits observed: {}", stake.credits_observed); - println!("epoch: {}", stake.epoch); - println!("activated stake: {}", stake.stake); - println!("previous stake: {}", stake.prev_stake); - Ok("".to_string()) - } - Ok(StakeState::MiningPool { epoch, point_value }) => { - println!("account lamports: {}", stake_account.lamports); - println!("epoch: {}", epoch); - println!("point_value: {}", point_value); + println!("stake: {}", stake.stake); Ok("".to_string()) } _ => Err(WalletError::RpcRequestError( @@ -1066,15 +1012,6 @@ pub fn process_command(config: &WalletConfig) -> ProcessResult { process_create_stake_account(&rpc_client, config, &staking_account_pubkey, *lamports) } - WalletCommand::CreateMiningPoolAccount(mining_pool_account_pubkey, lamports) => { - process_create_mining_pool_account( - &rpc_client, - config, - &mining_pool_account_pubkey, - *lamports, - ) - } - WalletCommand::DelegateStake(staking_account_keypair, voting_account_pubkey, lamports) => { process_delegate_stake( &rpc_client, @@ -1085,17 +1022,14 @@ pub fn process_command(config: &WalletConfig) -> ProcessResult { ) } - WalletCommand::RedeemVoteCredits( - mining_pool_account_pubkey, - staking_account_pubkey, - voting_account_pubkey, - ) => process_redeem_vote_credits( - &rpc_client, - config, - &mining_pool_account_pubkey, - &staking_account_pubkey, - &voting_account_pubkey, - ), + WalletCommand::RedeemVoteCredits(staking_account_pubkey, voting_account_pubkey) => { + process_redeem_vote_credits( + &rpc_client, + config, + &staking_account_pubkey, + &voting_account_pubkey, + ) + } WalletCommand::ShowStakeAccount(staking_account_pubkey) => { process_show_stake_account(&rpc_client, config, &staking_account_pubkey) @@ -1417,27 +1351,6 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .help("Vote account pubkey"), ) ) - .subcommand( - SubCommand::with_name("create-mining-pool-account") - .about("Create staking mining pool account") - .arg( - Arg::with_name("mining_pool_account_pubkey") - .index(1) - .value_name("PUBKEY") - .takes_value(true) - .required(true) - .validator(is_pubkey) - .help("Staking mining pool account address to fund"), - ) - .arg( - Arg::with_name("lamports") - .index(2) - .value_name("NUM") - .takes_value(true) - .required(true) - .help("The number of lamports to assign to the mining pool account"), - ), - ) .subcommand( SubCommand::with_name("create-stake-account") .about("Create staking account")