From 92d2452f33ea50e123d8776f2dec23b746c4014d Mon Sep 17 00:00:00 2001 From: Rob Walker Date: Wed, 11 Sep 2019 09:48:29 -0700 Subject: [PATCH] redelegate stake (#5868) * redelegate stake * boil this down to just delegate(), which can be offered any number of times --- programs/stake_api/src/config.rs | 3 + programs/stake_api/src/stake_instruction.rs | 74 ++++--- programs/stake_api/src/stake_state.rs | 210 +++++++++++--------- 3 files changed, 173 insertions(+), 114 deletions(-) diff --git a/programs/stake_api/src/config.rs b/programs/stake_api/src/config.rs index ffdeabe7f..6c4de54d4 100644 --- a/programs/stake_api/src/config.rs +++ b/programs/stake_api/src/config.rs @@ -24,8 +24,11 @@ pub const DEFAULT_SLASH_PENALTY: u8 = ((5 * std::u8::MAX as usize) / 100) as u8; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] pub struct Config { + /// how much stake we can activate per-epoch as a fraction of currently effective stake pub warmup_rate: f64, + /// how much stake we can deactivate as a fraction of currently effective stake pub cooldown_rate: f64, + /// percentage of stake lost when slash, expressed as a portion of std::u8::MAX pub slash_penalty: u8, } diff --git a/programs/stake_api/src/stake_instruction.rs b/programs/stake_api/src/stake_instruction.rs index 10d5e9eb0..d9e22acb6 100644 --- a/programs/stake_api/src/stake_instruction.rs +++ b/programs/stake_api/src/stake_instruction.rs @@ -41,27 +41,28 @@ pub enum StakeInstruction { /// Expects 1 Account: /// 0 - Uninitialized StakeAccount to be lockup'd /// - /// The u64 is the portion of the Stake account balance to be activated, - /// must be less than StakeAccount.lamports + /// The Slot parameter denotes slot height at which this stake + /// will allow withdrawal from the stake account. /// Lockup(Slot), - /// `Delegate` a stake to a particular node + /// `Delegate` a stake to a particular vote account /// - /// Expects 3 Accounts: - /// 0 - Lockup'd StakeAccount to be delegated <= must have this signature + /// Expects 4 Accounts: + /// 0 - Lockup'd StakeAccount to be delegated <= transaction must have this signature /// 1 - VoteAccount to which this Stake will be delegated /// 2 - Clock sysvar Account that carries clock bank epoch /// 3 - Config Account that carries stake config /// - /// The u64 is the portion of the Stake account balance to be activated, - /// must be less than StakeAccount.lamports + /// The entire balance of the staking account is staked. DelegateStake + /// can be called multiple times, but re-delegation is delayed + /// by one epoch /// - DelegateStake(u64), + DelegateStake, /// Redeem credits in the stake account /// - /// Expects 4 Accounts: + /// Expects 5 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 @@ -71,8 +72,8 @@ pub enum StakeInstruction { /// Withdraw unstaked lamports from the stake account /// - /// Expects 3 Accounts: - /// 0 - Delegate StakeAccount + /// Expects 4 Accounts: + /// 0 - Delegate StakeAccount <= transaction must have this signature /// 1 - System account to which the lamports will be transferred, /// 2 - Syscall Account that carries epoch /// 3 - StakeHistory sysvar that carries stake warmup/cooldown history @@ -83,10 +84,11 @@ pub enum StakeInstruction { /// Deactivates the stake in the account /// - /// Expects 2 Accounts: - /// 0 - Delegate StakeAccount + /// Expects 3 Accounts: + /// 0 - Delegate StakeAccount <= transaction must have this signature /// 1 - VoteAccount to which the Stake is delegated /// 2 - Syscall Account that carries epoch + /// Deactivate, } @@ -127,7 +129,7 @@ pub fn create_stake_account_and_delegate_stake( lamports: u64, ) -> Vec { let mut instructions = create_stake_account(from_pubkey, stake_pubkey, lamports); - instructions.push(delegate_stake(stake_pubkey, vote_pubkey, lamports)); + instructions.push(delegate_stake(stake_pubkey, vote_pubkey)); instructions } @@ -142,14 +144,14 @@ pub fn redeem_vote_credits(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey) -> Instr Instruction::new(id(), &StakeInstruction::RedeemVoteCredits, account_metas) } -pub fn delegate_stake(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey, stake: u64) -> Instruction { +pub fn delegate_stake(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey) -> Instruction { let account_metas = vec![ AccountMeta::new(*stake_pubkey, true), AccountMeta::new_credit_only(*vote_pubkey, false), AccountMeta::new_credit_only(sysvar::clock::id(), false), AccountMeta::new_credit_only(crate::config::id(), false), ]; - Instruction::new(id(), &StakeInstruction::DelegateStake(stake), account_metas) + Instruction::new(id(), &StakeInstruction::DelegateStake, account_metas) } pub fn withdraw(stake_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64) -> Instruction { @@ -191,7 +193,7 @@ pub fn process_instruction( // TODO: data-driven unpack and dispatch of KeyedAccounts match deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)? { StakeInstruction::Lockup(slot) => me.lockup(slot), - StakeInstruction::DelegateStake(stake) => { + StakeInstruction::DelegateStake => { if rest.len() != 3 { Err(InstructionError::InvalidInstructionData)?; } @@ -199,7 +201,6 @@ pub fn process_instruction( me.delegate_stake( vote, - stake, &sysvar::clock::from_keyed_account(&rest[1])?, &config::from_keyed_account(&rest[2])?, ) @@ -290,7 +291,7 @@ mod tests { Err(InstructionError::InvalidAccountData), ); assert_eq!( - process_instruction(&delegate_stake(&Pubkey::default(), &Pubkey::default(), 0)), + process_instruction(&delegate_stake(&Pubkey::default(), &Pubkey::default())), Err(InstructionError::InvalidAccountData), ); assert_eq!( @@ -307,7 +308,17 @@ mod tests { fn test_stake_process_instruction_decode_bail() { // these will not call stake_state, have bogus contents - // gets the first check + // gets the "is_empty()" check + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [], + &serialize(&StakeInstruction::Lockup(0)).unwrap(), + ), + Err(InstructionError::InvalidInstructionData), + ); + + // gets the first check in delegate, wrong number of accounts assert_eq!( super::process_instruction( &Pubkey::default(), @@ -316,7 +327,7 @@ mod tests { false, &mut Account::default(), )], - &serialize(&StakeInstruction::DelegateStake(0)).unwrap(), + &serialize(&StakeInstruction::DelegateStake).unwrap(), ), Err(InstructionError::InvalidInstructionData), ); @@ -330,11 +341,12 @@ mod tests { false, &mut Account::default() ),], - &serialize(&StakeInstruction::DelegateStake(0)).unwrap(), + &serialize(&StakeInstruction::DelegateStake).unwrap(), ), Err(InstructionError::InvalidInstructionData), ); + // catches the number of args check assert_eq!( super::process_instruction( &Pubkey::default(), @@ -347,6 +359,22 @@ mod tests { Err(InstructionError::InvalidInstructionData), ); + // catches the type of args check + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [ + 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(&Pubkey::default(), false, &mut Account::default()), + KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()), + ], + &serialize(&StakeInstruction::RedeemVoteCredits).unwrap(), + ), + Err(InstructionError::InvalidArgument), + ); + // gets the check non-deserialize-able account in delegate_stake assert_eq!( super::process_instruction( @@ -365,7 +393,7 @@ mod tests { &mut config::create_account(1, &config::Config::default()) ), ], - &serialize(&StakeInstruction::DelegateStake(0)).unwrap(), + &serialize(&StakeInstruction::DelegateStake).unwrap(), ), Err(InstructionError::InvalidAccountData), ); diff --git a/programs/stake_api/src/stake_state.rs b/programs/stake_api/src/stake_state.rs index 65c4002c9..65872cd07 100644 --- a/programs/stake_api/src/stake_state.rs +++ b/programs/stake_api/src/stake_state.rs @@ -76,7 +76,7 @@ pub struct Stake { pub prior_delegates_idx: usize, } -const MAX_PRIOR_DELEGATES: usize = 32; +const MAX_PRIOR_DELEGATES: usize = 32; // this is how many epochs a stake is exposed to a slashing condition impl Default for Stake { fn default() -> Self { @@ -90,7 +90,7 @@ impl Default for Stake { config: Config::default(), lockup: 0, prior_delegates: <[(Pubkey, Epoch, Epoch); MAX_PRIOR_DELEGATES]>::default(), - prior_delegates_idx: 0, + prior_delegates_idx: MAX_PRIOR_DELEGATES - 1, } } } @@ -104,8 +104,15 @@ impl Stake { self.stake_activating_and_deactivating(epoch, history).0 } - pub fn voter_pubkey(&self, _epoch: Epoch) -> &Pubkey { - &self.voter_pubkey + pub fn voter_pubkey(&self, epoch: Epoch) -> &Pubkey { + let prior_delegate_pubkey = &self.prior_delegates[self.prior_delegates_idx].0; + // next epoch from re-delegation, or no redelegations + if epoch > self.voter_pubkey_epoch || *prior_delegate_pubkey == Pubkey::default() { + &self.voter_pubkey + } else { + assert!(epoch <= self.prior_delegates[self.prior_delegates_idx].2); + prior_delegate_pubkey + } } fn stake_activating_and_deactivating( @@ -287,6 +294,26 @@ impl Stake { ) } + fn redelegate( + &mut self, + voter_pubkey: &Pubkey, + vote_state: &VoteState, + epoch: Epoch, + ) -> Result<(), StakeError> { + // remember old delegate, + if epoch != self.voter_pubkey_epoch { + self.prior_delegates_idx += 1; + self.prior_delegates_idx %= MAX_PRIOR_DELEGATES; + + self.prior_delegates[self.prior_delegates_idx] = + (self.voter_pubkey, self.voter_pubkey_epoch, epoch); + } + self.voter_pubkey = *voter_pubkey; + self.voter_pubkey_epoch = epoch; + self.credits_observed = vote_state.credits(); + Ok(()) + } + fn new( stake: u64, voter_pubkey: &Pubkey, @@ -299,6 +326,7 @@ impl Stake { stake, activation_epoch, voter_pubkey: *voter_pubkey, + voter_pubkey_epoch: activation_epoch, credits_observed: vote_state.credits(), config: *config, lockup, @@ -316,7 +344,6 @@ pub trait StakeAccount { fn delegate_stake( &mut self, vote_account: &KeyedAccount, - stake: u64, clock: &sysvar::clock::Clock, config: &Config, ) -> Result<(), InstructionError>; @@ -352,7 +379,6 @@ impl<'a> StakeAccount for KeyedAccount<'a> { fn delegate_stake( &mut self, vote_account: &KeyedAccount, - new_stake: u64, clock: &sysvar::clock::Clock, config: &Config, ) -> Result<(), InstructionError> { @@ -360,13 +386,9 @@ impl<'a> StakeAccount for KeyedAccount<'a> { return Err(InstructionError::MissingRequiredSignature); } - if new_stake > self.account.lamports { - return Err(InstructionError::InsufficientFunds); - } - if let StakeState::Lockup(lockup) = self.state()? { let stake = Stake::new( - new_stake, + self.account.lamports, vote_account.unsigned_key(), &vote_account.state()?, clock.epoch, @@ -374,6 +396,13 @@ impl<'a> StakeAccount for KeyedAccount<'a> { lockup, ); + self.set_state(&StakeState::Stake(stake)) + } else if let StakeState::Stake(mut stake) = self.state()? { + stake.redelegate( + vote_account.unsigned_key(), + &vote_account.state()?, + clock.epoch, + )?; self.set_state(&StakeState::Stake(stake)) } else { Err(InstructionError::InvalidAccountData) @@ -538,10 +567,7 @@ pub fn create_account(voter_pubkey: &Pubkey, vote_account: &Account, lamports: u mod tests { use super::*; use crate::id; - use solana_sdk::account::Account; - use solana_sdk::pubkey::Pubkey; - use solana_sdk::signature::{Keypair, KeypairUtil}; - use solana_sdk::system_program; + use solana_sdk::{account::Account, pubkey::Pubkey, system_program}; use solana_vote_api::vote_state; #[test] @@ -582,13 +608,12 @@ mod tests { ..sysvar::clock::Clock::default() }; - let vote_keypair = Keypair::new(); + let vote_pubkey = Pubkey::new_rand(); 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); @@ -613,66 +638,88 @@ mod tests { } assert_eq!( - stake_keyed_account.delegate_stake(&vote_keyed_account, 0, &clock, &Config::default()), + stake_keyed_account.delegate_stake(&vote_keyed_account, &clock, &Config::default()), Err(InstructionError::MissingRequiredSignature) ); // signed keyed account let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); assert!(stake_keyed_account - .delegate_stake( - &vote_keyed_account, - stake_lamports, - &clock, - &Config::default() - ) + .delegate_stake(&vote_keyed_account, &clock, &Config::default()) .is_ok()); // verify that delegate_stake() looks right, compare against hand-rolled - let stake_state: StakeState = stake_keyed_account.state().unwrap(); + let stake = StakeState::stake_from(&stake_keyed_account.account).unwrap(); assert_eq!( - stake_state, - StakeState::Stake(Stake { - voter_pubkey: vote_keypair.pubkey(), + stake, + Stake { + voter_pubkey: vote_pubkey, + voter_pubkey_epoch: clock.epoch, credits_observed: vote_state.credits(), stake: stake_lamports, activation_epoch: clock.epoch, deactivation_epoch: std::u64::MAX, ..Stake::default() - }) - ); - // verify that delegate_stake can't be called twice StakeState::default() - // signed keyed account - assert_eq!( - stake_keyed_account.delegate_stake( - &vote_keyed_account, - stake_lamports, - &clock, - &Config::default() - ), - Err(InstructionError::InvalidAccountData) + } ); - // verify can only stake up to account lamports - let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); - assert_eq!( - stake_keyed_account.delegate_stake( - &vote_keyed_account, - stake_lamports + 1, - &clock, - &Config::default() - ), - Err(InstructionError::InsufficientFunds) - ); + // verify that voter_pubkey() is right for all epochs, even ones that don't count (like 0) + for epoch in 0..=clock.epoch + 1 { + assert_eq!(stake.voter_pubkey(epoch), &vote_pubkey); + } + // verify that delegate_stake can be called twice, 2nd is redelegate + assert!(stake_keyed_account + .delegate_stake(&vote_keyed_account, &clock, &Config::default()) + .is_ok()); + + // verify that non-stakes fail delegate_stake() let stake_state = StakeState::RewardsPool; stake_keyed_account.set_state(&stake_state).unwrap(); assert!(stake_keyed_account - .delegate_stake(&vote_keyed_account, 0, &clock, &Config::default()) + .delegate_stake(&vote_keyed_account, &clock, &Config::default()) .is_err()); } + #[test] + fn test_stake_redelegate() { + // what a freshly delegated stake looks like + let mut stake = Stake { + voter_pubkey: Pubkey::new_rand(), + voter_pubkey_epoch: 0, + ..Stake::default() + }; + // verify that redelegation any number of times since first delegation works just fine, + // and that the stake is delegated to the most recent vote account + for epoch in 0..=MAX_PRIOR_DELEGATES + 1 { + let voter_pubkey = Pubkey::new_rand(); + let _ignored = stake.redelegate(&voter_pubkey, &VoteState::default(), 0); + assert_eq!(stake.voter_pubkey(epoch as u64), &voter_pubkey); + } + + // get a new voter_pubkey + let voter_pubkey = Pubkey::new_rand(); + // save off old voter_pubkey + let prior_voter_pubkey = stake.voter_pubkey; + + // actually redelegate in epoch 1 + let _ignored = stake.redelegate(&voter_pubkey, &VoteState::default(), 1); + // verify that delegation is delayed + assert_eq!(stake.voter_pubkey(0 as u64), &prior_voter_pubkey); + assert_eq!(stake.voter_pubkey(1 as u64), &prior_voter_pubkey); + assert_eq!(stake.voter_pubkey(2 as u64), &voter_pubkey); + + // verify that prior_delegates wraps around safely... + for epoch in 0..=MAX_PRIOR_DELEGATES + 1 { + let voter_pubkey = Pubkey::new_rand(); + let prior_voter_pubkey = stake.voter_pubkey; + let _ignored = stake.redelegate(&voter_pubkey, &VoteState::default(), epoch as u64); + assert_eq!(stake.voter_pubkey(epoch as u64), &prior_voter_pubkey); + assert_eq!(stake.voter_pubkey((epoch + 1) as u64), &voter_pubkey); + } + } + fn create_stake_history_from_stakes( bootstrap: Option, epochs: std::ops::Range, @@ -986,12 +1033,7 @@ mod tests { let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account); vote_keyed_account.set_state(&VoteState::default()).unwrap(); assert_eq!( - stake_keyed_account.delegate_stake( - &vote_keyed_account, - stake_lamports, - &clock, - &Config::default() - ), + stake_keyed_account.delegate_stake(&vote_keyed_account, &clock, &Config::default()), Ok(()) ); @@ -1005,10 +1047,9 @@ mod tests { #[test] fn test_withdraw_stake() { let stake_pubkey = Pubkey::new_rand(); - let total_lamports = 100; let stake_lamports = 42; let mut stake_account = Account::new_data_with_space( - total_lamports, + stake_lamports, &StakeState::Lockup(0), std::mem::size_of::(), &id(), @@ -1025,7 +1066,7 @@ mod tests { let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &mut stake_account); assert_eq!( stake_keyed_account.withdraw( - total_lamports, + stake_lamports, &mut to_keyed_account, &clock, &StakeHistory::default() @@ -1037,7 +1078,7 @@ mod tests { let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); assert_eq!( stake_keyed_account.withdraw( - total_lamports, + stake_lamports, &mut to_keyed_account, &clock, &StakeHistory::default() @@ -1047,13 +1088,13 @@ mod tests { assert_eq!(stake_account.lamports, 0); // reset balance - stake_account.lamports = total_lamports; + stake_account.lamports = stake_lamports; // signed keyed account and uninitialized, more than available should fail let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); assert_eq!( stake_keyed_account.withdraw( - total_lamports + 1, + stake_lamports + 1, &mut to_keyed_account, &clock, &StakeHistory::default() @@ -1061,41 +1102,38 @@ mod tests { Err(InstructionError::InsufficientFunds) ); - // Stake some lamports (available lampoorts for withdrawals will reduce) + // Stake some lamports (available lamports for withdrawals will reduce to zero) 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); vote_keyed_account.set_state(&VoteState::default()).unwrap(); assert_eq!( - stake_keyed_account.delegate_stake( - &vote_keyed_account, - stake_lamports, - &clock, - &Config::default() - ), + stake_keyed_account.delegate_stake(&vote_keyed_account, &clock, &Config::default()), Ok(()) ); - // withdrawal before deactivate works for some portion + // simulate rewards + stake_account.lamports += 10; + // withdrawal before deactivate works for rewards amount let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); assert_eq!( stake_keyed_account.withdraw( - total_lamports - stake_lamports, + 10, &mut to_keyed_account, &clock, &StakeHistory::default() ), Ok(()) ); - // reset balance - stake_account.lamports = total_lamports; - // withdrawal before deactivate fails if not in excess of stake + // simulate rewards + stake_account.lamports += 10; + // withdrawal of rewards fails if not in excess of stake let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); assert_eq!( stake_keyed_account.withdraw( - total_lamports - stake_lamports + 1, + 10 + 1, &mut to_keyed_account, &clock, &StakeHistory::default() @@ -1114,7 +1152,7 @@ mod tests { // Try to withdraw more than what's available assert_eq!( stake_keyed_account.withdraw( - total_lamports + 1, + stake_lamports + 10 + 1, &mut to_keyed_account, &clock, &StakeHistory::default() @@ -1125,7 +1163,7 @@ mod tests { // Try to withdraw all lamports assert_eq!( stake_keyed_account.withdraw( - total_lamports, + stake_lamports + 10, &mut to_keyed_account, &clock, &StakeHistory::default() @@ -1165,12 +1203,7 @@ mod tests { let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account); vote_keyed_account.set_state(&VoteState::default()).unwrap(); assert_eq!( - stake_keyed_account.delegate_stake( - &vote_keyed_account, - stake_lamports, - &future, - &Config::default() - ), + stake_keyed_account.delegate_stake(&vote_keyed_account, &future, &Config::default()), Ok(()) ); @@ -1382,12 +1415,7 @@ mod tests { // delegate the stake assert!(stake_keyed_account - .delegate_stake( - &vote_keyed_account, - stake_lamports, - &clock, - &Config::default() - ) + .delegate_stake(&vote_keyed_account, &clock, &Config::default()) .is_ok()); let stake_history = create_stake_history_from_stakes(