From 933e83583872022fa52ca4df2119fb851ab6a308 Mon Sep 17 00:00:00 2001 From: Rob Walker Date: Wed, 4 Sep 2019 13:34:09 -0700 Subject: [PATCH] add stake lockup (#5782) * add stake lockup * fixup --- ci/test-stable.sh | 2 +- cli/src/wallet.rs | 4 +- programs/stake_api/src/stake_instruction.rs | 52 ++++-- programs/stake_api/src/stake_state.rs | 174 ++++++++++++++++---- runtime/src/append_vec.rs | 2 +- sdk/src/account.rs | 16 +- sdk/src/lib.rs | 3 - 7 files changed, 203 insertions(+), 50 deletions(-) diff --git a/ci/test-stable.sh b/ci/test-stable.sh index 57dfad033..d0f9657e3 100755 --- a/ci/test-stable.sh +++ b/ci/test-stable.sh @@ -19,7 +19,7 @@ source scripts/ulimit-n.sh # Clear cached json keypair files rm -rf "$HOME/.config/solana" -# Clear the C dependency files, if dependeny moves these files are not regenerated +# Clear the C dependency files, if dependency moves these files are not regenerated test -d target/debug/bpf && find target/debug/bpf -name '*.d' -delete test -d target/release/bpf && find target/release/bpf -name '*.d' -delete diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs index 8e920b504..d6ae657e1 100644 --- a/cli/src/wallet.rs +++ b/cli/src/wallet.rs @@ -895,7 +895,9 @@ fn process_show_stake_account( Ok("".to_string()) } Ok(StakeState::RewardsPool) => Ok("Stake account is a rewards pool".to_string()), - Ok(StakeState::Uninitialized) => Ok("Stake account is uninitialized".to_string()), + Ok(StakeState::Uninitialized) | Ok(StakeState::Lockup(_)) => { + Ok("Stake account is uninitialized".to_string()) + } Err(err) => Err(WalletError::RpcRequestError(format!( "Account data could not be deserialized to stake state: {:?}", err diff --git a/programs/stake_api/src/stake_instruction.rs b/programs/stake_api/src/stake_instruction.rs index d9fef6482..2101419c1 100644 --- a/programs/stake_api/src/stake_instruction.rs +++ b/programs/stake_api/src/stake_instruction.rs @@ -10,14 +10,25 @@ use solana_sdk::{ instruction::{AccountMeta, Instruction, InstructionError}, pubkey::Pubkey, system_instruction, sysvar, + timing::Slot, }; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum StakeInstruction { + /// `Lockup` a stake until the specified slot + /// + /// 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 + /// + Lockup(Slot), + /// `Delegate` a stake to a particular node /// /// Expects 3 Accounts: - /// 0 - Uninitialized StakeAccount to be delegated <= must have this signature + /// 0 - Lockup'd StakeAccount to be delegated <= 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 @@ -58,28 +69,44 @@ pub enum StakeInstruction { Deactivate, } +pub fn create_stake_account_with_lockup( + from_pubkey: &Pubkey, + stake_pubkey: &Pubkey, + lamports: u64, + lockup: Slot, +) -> Vec { + vec![ + system_instruction::create_account( + from_pubkey, + stake_pubkey, + lamports, + std::mem::size_of::() as u64, + &id(), + ), + Instruction::new( + id(), + &StakeInstruction::Lockup(lockup), + vec![AccountMeta::new(*stake_pubkey, false)], + ), + ] +} + pub fn create_stake_account( from_pubkey: &Pubkey, - staker_pubkey: &Pubkey, + stake_pubkey: &Pubkey, lamports: u64, ) -> Vec { - vec![system_instruction::create_account( - from_pubkey, - staker_pubkey, - lamports, - std::mem::size_of::() as u64, - &id(), - )] + create_stake_account_with_lockup(from_pubkey, stake_pubkey, lamports, 0) } pub fn create_stake_account_and_delegate_stake( from_pubkey: &Pubkey, - staker_pubkey: &Pubkey, + stake_pubkey: &Pubkey, vote_pubkey: &Pubkey, lamports: u64, ) -> Vec { - let mut instructions = create_stake_account(from_pubkey, staker_pubkey, lamports); - instructions.push(delegate_stake(staker_pubkey, vote_pubkey, lamports)); + let mut instructions = create_stake_account(from_pubkey, stake_pubkey, lamports); + instructions.push(delegate_stake(stake_pubkey, vote_pubkey, lamports)); instructions } @@ -142,6 +169,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) => { if rest.len() != 3 { Err(InstructionError::InvalidInstructionData)?; diff --git a/programs/stake_api/src/stake_state.rs b/programs/stake_api/src/stake_state.rs index c1c5ad09c..220a98e3d 100644 --- a/programs/stake_api/src/stake_state.rs +++ b/programs/stake_api/src/stake_state.rs @@ -14,13 +14,14 @@ use solana_sdk::{ self, stake_history::{StakeHistory, StakeHistoryEntry}, }, - timing::Epoch, + timing::{Epoch, Slot}, }; use solana_vote_api::vote_state::VoteState; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub enum StakeState { Uninitialized, + Lockup(Slot), Stake(Stake), RewardsPool, } @@ -57,6 +58,7 @@ pub struct Stake { pub activation_epoch: Epoch, // epoch the stake was activated, std::Epoch::MAX if is a bootstrap stake pub deactivation_epoch: Epoch, // epoch the stake was deactivated, std::Epoch::MAX if not deactivated pub config: Config, + pub lockup: Slot, } impl Default for Stake { @@ -68,6 +70,7 @@ impl Default for Stake { activation_epoch: 0, deactivation_epoch: std::u64::MAX, config: Config::default(), + lockup: 0, } } } @@ -256,6 +259,7 @@ impl Stake { vote_state, std::u64::MAX, &Config::default(), + 0, ) } @@ -265,6 +269,7 @@ impl Stake { vote_state: &VoteState, activation_epoch: Epoch, config: &Config, + lockup: Slot, ) -> Self { Self { stake, @@ -272,6 +277,7 @@ impl Stake { voter_pubkey: *voter_pubkey, credits_observed: vote_state.credits(), config: *config, + lockup, ..Stake::default() } } @@ -282,6 +288,7 @@ impl Stake { } pub trait StakeAccount { + fn lockup(&mut self, slot: Slot) -> Result<(), InstructionError>; fn delegate_stake( &mut self, vote_account: &KeyedAccount, @@ -311,6 +318,13 @@ pub trait StakeAccount { } impl<'a> StakeAccount for KeyedAccount<'a> { + fn lockup(&mut self, lockup: Slot) -> Result<(), InstructionError> { + if let StakeState::Uninitialized = self.state()? { + self.set_state(&StakeState::Lockup(lockup)) + } else { + Err(InstructionError::InvalidAccountData) + } + } fn delegate_stake( &mut self, vote_account: &KeyedAccount, @@ -326,13 +340,14 @@ impl<'a> StakeAccount for KeyedAccount<'a> { return Err(InstructionError::InsufficientFunds); } - if let StakeState::Uninitialized = self.state()? { + if let StakeState::Lockup(lockup) = self.state()? { let stake = Stake::new( new_stake, vote_account.unsigned_key(), &vote_account.state()?, clock.epoch, config, + lockup, ); self.set_state(&StakeState::Stake(stake)) @@ -410,6 +425,19 @@ impl<'a> StakeAccount for KeyedAccount<'a> { return Err(InstructionError::MissingRequiredSignature); } + fn transfer( + from: &mut Account, + to: &mut Account, + lamports: u64, + ) -> Result<(), InstructionError> { + if lamports > from.lamports { + return Err(InstructionError::InsufficientFunds); + } + from.lamports -= lamports; + to.lamports += lamports; + Ok(()) + } + match self.state()? { StakeState::Stake(stake) => { // if we have a deactivation epoch and we're in cooldown @@ -425,20 +453,16 @@ impl<'a> StakeAccount for KeyedAccount<'a> { if lamports > self.account.lamports.saturating_sub(staked) { return Err(InstructionError::InsufficientFunds); } - self.account.lamports -= lamports; - to.account.lamports += lamports; - Ok(()) } - StakeState::Uninitialized => { - if lamports > self.account.lamports { + StakeState::Lockup(lockup) => { + if lockup > clock.slot { return Err(InstructionError::InsufficientFunds); } - self.account.lamports -= lamports; - to.account.lamports += lamports; - Ok(()) } - _ => Err(InstructionError::InvalidAccountData), + StakeState::Uninitialized => {} + _ => return Err(InstructionError::InvalidAccountData), } + transfer(&mut self.account, &mut to.account, lamports) } } @@ -546,15 +570,20 @@ mod tests { let stake_pubkey = Pubkey::default(); let stake_lamports = 42; - let mut stake_account = - Account::new(stake_lamports, std::mem::size_of::(), &id()); + let mut stake_account = Account::new_data_with_space( + stake_lamports, + &StakeState::Lockup(0), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); // unsigned keyed account let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &mut stake_account); { let stake_state: StakeState = stake_keyed_account.state().unwrap(); - assert_eq!(stake_state, StakeState::default()); + assert_eq!(stake_state, StakeState::Lockup(0)); } assert_eq!( @@ -583,7 +612,8 @@ mod tests { stake: stake_lamports, activation_epoch: clock.epoch, deactivation_epoch: std::u64::MAX, - config: Config::default() + config: Config::default(), + lockup: 0 }) ); // verify that delegate_stake can't be called twice StakeState::default() @@ -865,12 +895,41 @@ mod tests { } #[test] - fn test_deactivate_stake() { + fn test_stake_lockup() { let stake_pubkey = Pubkey::new_rand(); let stake_lamports = 42; let mut stake_account = Account::new(stake_lamports, std::mem::size_of::(), &id()); + // unsigned keyed account + let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &mut stake_account); + assert_eq!(stake_keyed_account.lockup(1), Ok(())); + + // first time works, as is uninit + assert_eq!( + StakeState::from(&stake_keyed_account.account).unwrap(), + StakeState::Lockup(1) + ); + + // 2nd time fails, can't move it from anything other than uninit->lockup + assert_eq!( + stake_keyed_account.lockup(1), + Err(InstructionError::InvalidAccountData) + ); + } + + #[test] + fn test_deactivate_stake() { + let stake_pubkey = Pubkey::new_rand(); + let stake_lamports = 42; + let mut stake_account = Account::new_data_with_space( + stake_lamports, + &StakeState::Lockup(0), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + let clock = sysvar::clock::Clock { epoch: 1, ..sysvar::clock::Clock::default() @@ -923,8 +982,13 @@ mod tests { let stake_pubkey = Pubkey::new_rand(); let total_lamports = 100; let stake_lamports = 42; - let mut stake_account = - Account::new(total_lamports, std::mem::size_of::(), &id()); + let mut stake_account = Account::new_data_with_space( + total_lamports, + &StakeState::Lockup(0), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); let mut clock = sysvar::clock::Clock::default(); @@ -1051,8 +1115,13 @@ mod tests { let stake_pubkey = Pubkey::new_rand(); let total_lamports = 100; let stake_lamports = 42; - let mut stake_account = - Account::new(total_lamports, std::mem::size_of::(), &id()); + let mut stake_account = Account::new_data_with_space( + total_lamports, + &StakeState::Lockup(0), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); let clock = sysvar::clock::Clock::default(); let mut future = sysvar::clock::Clock::default(); @@ -1102,21 +1171,49 @@ mod tests { fn test_withdraw_stake_invalid_state() { let stake_pubkey = Pubkey::new_rand(); let total_lamports = 100; - let mut stake_account = - Account::new(total_lamports, std::mem::size_of::(), &id()); + let mut stake_account = Account::new_data_with_space( + total_lamports, + &StakeState::RewardsPool, + std::mem::size_of::(), + &id(), + ) + .expect("stake_account");; - let clock = sysvar::clock::Clock::default(); - let mut future = sysvar::clock::Clock::default(); - future.epoch += 16; + let to = Pubkey::new_rand(); + let mut to_account = Account::new(1, 0, &system_program::id()); + let mut to_keyed_account = KeyedAccount::new(&to, false, &mut to_account); + let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); + + assert_eq!( + stake_keyed_account.withdraw( + total_lamports, + &mut to_keyed_account, + &sysvar::clock::Clock::default(), + &StakeHistory::default() + ), + Err(InstructionError::InvalidAccountData) + ); + } + + #[test] + fn test_withdraw_lockout() { + let stake_pubkey = Pubkey::new_rand(); + let total_lamports = 100; + let mut stake_account = Account::new_data_with_space( + total_lamports, + &StakeState::Lockup(1), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); let to = Pubkey::new_rand(); let mut to_account = Account::new(1, 0, &system_program::id()); let mut to_keyed_account = KeyedAccount::new(&to, false, &mut to_account); let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); - let stake_state = StakeState::RewardsPool; - stake_keyed_account.set_state(&stake_state).unwrap(); + let mut clock = sysvar::clock::Clock::default(); assert_eq!( stake_keyed_account.withdraw( total_lamports, @@ -1124,7 +1221,18 @@ mod tests { &clock, &StakeHistory::default() ), - Err(InstructionError::InvalidAccountData) + Err(InstructionError::InsufficientFunds) + ); + + clock.slot += 1; + assert_eq!( + stake_keyed_account.withdraw( + total_lamports, + &mut to_keyed_account, + &clock, + &StakeHistory::default() + ), + Ok(()) ); } @@ -1221,8 +1329,14 @@ mod tests { let pubkey = Pubkey::default(); let stake_lamports = 100; - let mut stake_account = - Account::new(stake_lamports, std::mem::size_of::(), &id()); + let mut stake_account = Account::new_data_with_space( + stake_lamports, + &StakeState::Lockup(0), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + let mut stake_keyed_account = KeyedAccount::new(&pubkey, true, &mut stake_account); let vote_pubkey = Pubkey::new_rand(); diff --git a/runtime/src/append_vec.rs b/runtime/src/append_vec.rs index a7b8e9345..042dbf30f 100644 --- a/runtime/src/append_vec.rs +++ b/runtime/src/append_vec.rs @@ -1,7 +1,7 @@ use bincode::{deserialize_from, serialize_into, serialized_size}; use memmap::MmapMut; use serde::{Deserialize, Serialize}; -use solana_sdk::{account::Account, pubkey::Pubkey, Epoch}; +use solana_sdk::{account::Account, pubkey::Pubkey, timing::Epoch}; use std::fmt; use std::fs::{create_dir_all, remove_file, OpenOptions}; use std::io; diff --git a/sdk/src/account.rs b/sdk/src/account.rs index 9a17944d1..dbb331f06 100644 --- a/sdk/src/account.rs +++ b/sdk/src/account.rs @@ -1,5 +1,4 @@ -use crate::pubkey::Pubkey; -use crate::Epoch; +use crate::{pubkey::Pubkey, timing::Epoch}; use std::{cmp, fmt}; /// An Account with data that is stored on chain @@ -64,6 +63,19 @@ impl Account { }) } + pub fn new_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result { + let mut account = Self::new(lamports, space, owner); + + account.serialize_data(state)?; + + Ok(account) + } + pub fn deserialize_data(&self) -> Result { bincode::deserialize(&self.data) } diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 852a0cdf0..24287c92b 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -28,6 +28,3 @@ pub mod transport; #[macro_use] extern crate serde_derive; - -pub type Epoch = u64; -pub type Slot = u64;