diff --git a/cli/src/stake.rs b/cli/src/stake.rs index b59c86213e..44b571e602 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -20,7 +20,7 @@ use solana_sdk::{ }; use solana_stake_api::{ stake_instruction::{self, StakeError}, - stake_state::{Authorized, Lockup, StakeAuthorize, StakeState}, + stake_state::{Authorized, Lockup, Meta, StakeAuthorize, StakeState}, }; use solana_vote_api::vote_state::VoteState; use std::ops::Deref; @@ -548,7 +548,12 @@ pub fn process_show_stake_account( println!("lockup custodian: {}", lockup.custodian); } match stake_account.state() { - Ok(StakeState::Stake(authorized, lockup, stake)) => { + Ok(StakeState::Stake( + Meta { + authorized, lockup, .. + }, + stake, + )) => { println!( "total stake: {}", build_balance_message(stake_account.lamports, use_lamports_unit, true) @@ -577,7 +582,9 @@ pub fn process_show_stake_account( } Ok(StakeState::RewardsPool) => Ok("Stake account is a rewards pool".to_string()), Ok(StakeState::Uninitialized) => Ok("Stake account is uninitialized".to_string()), - Ok(StakeState::Initialized(authorized, lockup)) => { + Ok(StakeState::Initialized(Meta { + authorized, lockup, .. + })) => { println!("Stake account is undelegated"); show_authorized(&authorized); show_lockup(&lockup); diff --git a/programs/stake_api/src/stake_instruction.rs b/programs/stake_api/src/stake_instruction.rs index 4b53625211..ea27f46f8c 100644 --- a/programs/stake_api/src/stake_instruction.rs +++ b/programs/stake_api/src/stake_instruction.rs @@ -11,7 +11,6 @@ use solana_sdk::{ instruction_processor_utils::{limited_deserialize, next_keyed_account, DecodeError}, pubkey::Pubkey, system_instruction, sysvar, - sysvar::rent, }; /// Reasons the stake might have had an error @@ -21,6 +20,7 @@ pub enum StakeError { LockupInForce, AlreadyDeactivated, TooSoonToRedelegate, + InsufficientStake, } impl DecodeError for StakeError { fn type_of() -> &'static str { @@ -33,9 +33,8 @@ impl std::fmt::Display for StakeError { StakeError::NoCreditsToRedeem => write!(f, "not enough credits to redeem"), StakeError::LockupInForce => write!(f, "lockup has not yet expired"), StakeError::AlreadyDeactivated => write!(f, "stake already deactivated"), - StakeError::TooSoonToRedelegate => { - write!(f, "only one redelegation permitted per epoch") - } + StakeError::TooSoonToRedelegate => write!(f, "one re-delegation permitted per epoch"), + StakeError::InsufficientStake => write!(f, "split amount is more than is staked"), } } } @@ -45,8 +44,9 @@ impl std::error::Error for StakeError {} pub enum StakeInstruction { /// `Initialize` a stake with Lockup and Authorized information /// - /// Expects 1 Account: + /// Expects 2 Accounts: /// 0 - Uninitialized StakeAccount + /// 1 - Rent sysvar /// /// Authorized carries pubkeys that must sign staker transactions /// and withdrawer transactions. @@ -87,8 +87,24 @@ pub enum StakeInstruction { /// 2 - RewardsPool Stake Account from which to redeem credits /// 3 - Rewards sysvar Account that carries points values /// 4 - StakeHistory sysvar that carries stake warmup/cooldown history + /// RedeemVoteCredits, + /// Split u64 tokens and stake off a stake account into another stake + /// account. Requires Authorized::staker signature. + /// + /// The split-off stake account must be Initialized and carry the + /// the same values for Lockup and Authorized as the source + /// or this instruction will fail. + /// + /// The source stake must be either Initialized or a Stake. + /// + /// Expects 2 Accounts: + /// 0 - StakeAccount to be split + /// 1 - Initialized StakeAcount that will take the split-off amount + /// + Split(u64), + /// Withdraw unstaked lamports from the stake account /// requires Authorized::withdrawer signature /// @@ -112,6 +128,17 @@ pub enum StakeInstruction { Deactivate, } +pub fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Lockup) -> Instruction { + Instruction::new( + id(), + &StakeInstruction::Initialize(*authorized, *lockup), + vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_credit_only(sysvar::rent::id(), false), + ], + ) +} + pub fn create_stake_account_with_lockup( from_pubkey: &Pubkey, stake_pubkey: &Pubkey, @@ -127,13 +154,34 @@ pub fn create_stake_account_with_lockup( std::mem::size_of::() as u64, &id(), ), + initialize(stake_pubkey, authorized, lockup), + ] +} + +pub fn split( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + lamports: u64, + split_stake_pubkey: &Pubkey, +) -> Vec { + vec![ + system_instruction::create_account( + stake_pubkey, + split_stake_pubkey, + 0, // creates an ephemeral, uninitialized Stake + std::mem::size_of::() as u64, + &id(), + ), Instruction::new( id(), - &StakeInstruction::Initialize(*authorized, *lockup), - vec![ - AccountMeta::new(*stake_pubkey, false), - AccountMeta::new(sysvar::rent::id(), false), - ], + &StakeInstruction::Split(lamports), + metas_with_signer( + &[ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new(*split_stake_pubkey, false), + ], + authorized_pubkey, + ), ), ] } @@ -279,10 +327,11 @@ pub fn process_instruction( // TODO: data-driven unpack and dispatch of KeyedAccounts match limited_deserialize(data)? { - StakeInstruction::Initialize(authorized, lockup) => { - rent::verify_rent_exemption(me, next_keyed_account(keyed_accounts)?)?; - me.initialize(&authorized, &lockup) - } + StakeInstruction::Initialize(authorized, lockup) => me.initialize( + &authorized, + &lockup, + &sysvar::rent::from_keyed_account(next_keyed_account(keyed_accounts)?)?, + ), StakeInstruction::Authorize(authorized_pubkey, stake_authorize) => { me.authorize(&authorized_pubkey, stake_authorize, &signers) } @@ -307,6 +356,11 @@ pub fn process_instruction( &sysvar::stake_history::from_keyed_account(next_keyed_account(keyed_accounts)?)?, ) } + StakeInstruction::Split(lamports) => { + let split_stake = &mut next_keyed_account(keyed_accounts)?; + me.split(lamports, split_stake, &signers) + } + StakeInstruction::Withdraw(lamports) => { let to = &mut next_keyed_account(keyed_accounts)?; me.withdraw( @@ -364,10 +418,38 @@ mod tests { #[test] fn test_stake_process_instruction() { + assert_eq!( + process_instruction(&initialize( + &Pubkey::default(), + &Authorized::default(), + &Lockup::default() + )), + Err(InstructionError::InvalidAccountData), + ); assert_eq!( process_instruction(&redeem_vote_credits(&Pubkey::default(), &Pubkey::default())), Err(InstructionError::InvalidAccountData), ); + assert_eq!( + process_instruction(&authorize( + &Pubkey::default(), + &Pubkey::default(), + &Pubkey::default(), + StakeAuthorize::Staker + )), + Err(InstructionError::InvalidAccountData), + ); + assert_eq!( + process_instruction( + &split( + &Pubkey::default(), + &Pubkey::default(), + 100, + &Pubkey::default() + )[1] + ), + Err(InstructionError::InvalidAccountData), + ); assert_eq!( process_instruction(&delegate_stake( &Pubkey::default(), @@ -409,6 +491,62 @@ mod tests { Err(InstructionError::NotEnoughAccountKeys), ); + // no account for rent + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [KeyedAccount::new( + &Pubkey::default(), + false, + &mut Account::default(), + )], + &serialize(&StakeInstruction::Initialize( + Authorized::default(), + Lockup::default() + )) + .unwrap(), + ), + Err(InstructionError::NotEnoughAccountKeys), + ); + + // rent fails to deserialize + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [ + KeyedAccount::new(&Pubkey::default(), false, &mut Account::default(),), + KeyedAccount::new(&sysvar::rent::id(), false, &mut Account::default(),) + ], + &serialize(&StakeInstruction::Initialize( + Authorized::default(), + Lockup::default() + )) + .unwrap(), + ), + Err(InstructionError::InvalidArgument), + ); + + // fails to deserialize stake state + assert_eq!( + super::process_instruction( + &Pubkey::default(), + &mut [ + KeyedAccount::new(&Pubkey::default(), false, &mut Account::default(),), + KeyedAccount::new( + &sysvar::rent::id(), + false, + &mut sysvar::rent::create_account(0, &Rent::default()) + ) + ], + &serialize(&StakeInstruction::Initialize( + Authorized::default(), + Lockup::default() + )) + .unwrap(), + ), + Err(InstructionError::InvalidAccountData), + ); + // gets the first check in delegate, wrong number of accounts assert_eq!( super::process_instruction( diff --git a/programs/stake_api/src/stake_state.rs b/programs/stake_api/src/stake_state.rs index 84e0a8cc3c..25fcb54ca5 100644 --- a/programs/stake_api/src/stake_state.rs +++ b/programs/stake_api/src/stake_state.rs @@ -8,9 +8,10 @@ use serde_derive::{Deserialize, Serialize}; use solana_sdk::{ account::{Account, KeyedAccount}, account_utils::State, - clock::{Epoch, Slot}, + clock::{Clock, Epoch, Slot}, instruction::InstructionError, pubkey::Pubkey, + rent::Rent, sysvar::{ self, stake_history::{StakeHistory, StakeHistoryEntry}, @@ -23,8 +24,8 @@ use std::collections::HashSet; #[allow(clippy::large_enum_variant)] pub enum StakeState { Uninitialized, - Initialized(Authorized, Lockup), - Stake(Authorized, Lockup, Stake), + Initialized(Meta), + Stake(Meta, Stake), RewardsPool, } @@ -50,13 +51,14 @@ impl StakeState { pub fn stake(&self) -> Option { match self { - StakeState::Stake(_authorized, _lockup, stake) => Some(*stake), + StakeState::Stake(_meta, stake) => Some(*stake), _ => None, } } pub fn authorized(&self) -> Option { match self { - StakeState::Stake(authorized, _lockup, _stake) => Some(*authorized), + StakeState::Stake(meta, _stake) => Some(meta.authorized), + StakeState::Initialized(meta) => Some(meta.authorized), _ => None, } } @@ -85,6 +87,23 @@ pub struct Authorized { pub withdrawer: Pubkey, } +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] +pub struct Meta { + pub rent_exempt_reserve: u64, + pub authorized: Authorized, + pub lockup: Lockup, +} + +impl Meta { + pub fn auto(authorized: &Pubkey) -> Self { + Self { + authorized: Authorized::auto(authorized), + rent_exempt_reserve: Rent::default().minimum_balance(std::mem::size_of::()), + ..Meta::default() + } + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] pub struct Stake { /// most recently delegated vote account pubkey @@ -351,7 +370,7 @@ impl Stake { &mut self, voter_pubkey: &Pubkey, vote_state: &VoteState, - clock: &sysvar::clock::Clock, + clock: &Clock, ) -> Result<(), StakeError> { // only one re-delegation supported per epoch if self.voter_pubkey_epoch == clock.epoch { @@ -375,6 +394,18 @@ impl Stake { Ok(()) } + fn split(&mut self, lamports: u64) -> Result { + if lamports > self.stake { + return Err(StakeError::InsufficientStake); + } + self.stake -= lamports; + let new = Self { + stake: lamports, + ..*self + }; + Ok(new) + } + fn new( stake: u64, voter_pubkey: &Pubkey, @@ -408,6 +439,7 @@ pub trait StakeAccount { &mut self, authorized: &Authorized, lockup: &Lockup, + rent: &Rent, ) -> Result<(), InstructionError>; fn authorize( &mut self, @@ -434,6 +466,12 @@ pub trait StakeAccount { rewards: &sysvar::rewards::Rewards, stake_history: &sysvar::stake_history::StakeHistory, ) -> Result<(), InstructionError>; + fn split( + &mut self, + lamports: u64, + split_stake: &mut KeyedAccount, + signers: &HashSet, + ) -> Result<(), InstructionError>; fn withdraw( &mut self, lamports: u64, @@ -449,13 +487,25 @@ impl<'a> StakeAccount for KeyedAccount<'a> { &mut self, authorized: &Authorized, lockup: &Lockup, + rent: &Rent, ) -> Result<(), InstructionError> { if let StakeState::Uninitialized = self.state()? { - self.set_state(&StakeState::Initialized(*authorized, *lockup)) + let rent_exempt_reserve = rent.minimum_balance(self.account.data.len()); + + if rent_exempt_reserve < self.account.lamports { + self.set_state(&StakeState::Initialized(Meta { + rent_exempt_reserve, + authorized: *authorized, + lockup: *lockup, + })) + } else { + Err(InstructionError::InsufficientFunds) + } } else { Err(InstructionError::InvalidAccountData) } } + /// Authorize the given pubkey to manage stake (deactivate, withdraw). This may be called /// multiple times, but will implicitly withdraw authorization from the previously authorized /// staker. The default staker is the owner of the stake account's pubkey. @@ -465,16 +515,18 @@ impl<'a> StakeAccount for KeyedAccount<'a> { stake_authorize: StakeAuthorize, signers: &HashSet, ) -> Result<(), InstructionError> { - let stake_state = self.state()?; - - if let StakeState::Stake(mut authorized, lockup, stake) = stake_state { - authorized.authorize(signers, authority, stake_authorize)?; - self.set_state(&StakeState::Stake(authorized, lockup, stake)) - } else if let StakeState::Initialized(mut authorized, lockup) = stake_state { - authorized.authorize(signers, authority, stake_authorize)?; - self.set_state(&StakeState::Initialized(authorized, lockup)) - } else { - Err(InstructionError::InvalidAccountData) + match self.state()? { + StakeState::Stake(mut meta, stake) => { + meta.authorized + .authorize(signers, authority, stake_authorize)?; + self.set_state(&StakeState::Stake(meta, stake)) + } + StakeState::Initialized(mut meta) => { + meta.authorized + .authorize(signers, authority, stake_authorize)?; + self.set_state(&StakeState::Initialized(meta)) + } + _ => Err(InstructionError::InvalidAccountData), } } fn delegate_stake( @@ -484,23 +536,26 @@ impl<'a> StakeAccount for KeyedAccount<'a> { config: &Config, signers: &HashSet, ) -> Result<(), InstructionError> { - if let StakeState::Initialized(authorized, lockup) = self.state()? { - authorized.check(signers, StakeAuthorize::Staker)?; - let stake = Stake::new( - self.account.lamports, - vote_account.unsigned_key(), - &vote_account.state()?, - clock.epoch, - config, - ); - - self.set_state(&StakeState::Stake(authorized, lockup, stake)) - } else if let StakeState::Stake(authorized, lockup, mut stake) = self.state()? { - authorized.check(signers, StakeAuthorize::Staker)?; - stake.redelegate(vote_account.unsigned_key(), &vote_account.state()?, &clock)?; - self.set_state(&StakeState::Stake(authorized, lockup, stake)) - } else { - Err(InstructionError::InvalidAccountData) + match self.state()? { + StakeState::Initialized(meta) => { + meta.authorized.check(signers, StakeAuthorize::Staker)?; + let stake = Stake::new( + self.account + .lamports + .saturating_sub(meta.rent_exempt_reserve), // can't stake the rent ;) + vote_account.unsigned_key(), + &vote_account.state()?, + clock.epoch, + config, + ); + self.set_state(&StakeState::Stake(meta, stake)) + } + StakeState::Stake(meta, mut stake) => { + meta.authorized.check(signers, StakeAuthorize::Staker)?; + stake.redelegate(vote_account.unsigned_key(), &vote_account.state()?, &clock)?; + self.set_state(&StakeState::Stake(meta, stake)) + } + _ => Err(InstructionError::InvalidAccountData), } } fn deactivate_stake( @@ -508,11 +563,11 @@ impl<'a> StakeAccount for KeyedAccount<'a> { clock: &sysvar::clock::Clock, signers: &HashSet, ) -> Result<(), InstructionError> { - if let StakeState::Stake(authorized, lockup, mut stake) = self.state()? { - authorized.check(signers, StakeAuthorize::Staker)?; + if let StakeState::Stake(meta, mut stake) = self.state()? { + meta.authorized.check(signers, StakeAuthorize::Staker)?; stake.deactivate(clock.epoch)?; - self.set_state(&StakeState::Stake(authorized, lockup, stake)) + self.set_state(&StakeState::Stake(meta, stake)) } else { Err(InstructionError::InvalidAccountData) } @@ -524,7 +579,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { rewards: &sysvar::rewards::Rewards, stake_history: &sysvar::stake_history::StakeHistory, ) -> Result<(), InstructionError> { - if let (StakeState::Stake(authorized, lockup, mut stake), StakeState::RewardsPool) = + if let (StakeState::Stake(meta, mut stake), StakeState::RewardsPool) = (self.state()?, rewards_account.state()?) { let vote_state: VoteState = vote_account.state()?; @@ -553,7 +608,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { stake.credits_observed = credits_observed; stake.stake += stakers_reward; - self.set_state(&StakeState::Stake(authorized, lockup, stake)) + self.set_state(&StakeState::Stake(meta, stake)) } else { // not worth collecting Err(StakeError::NoCreditsToRedeem.into()) @@ -562,6 +617,75 @@ impl<'a> StakeAccount for KeyedAccount<'a> { Err(InstructionError::InvalidAccountData) } } + + fn split( + &mut self, + lamports: u64, + split: &mut KeyedAccount, + signers: &HashSet, + ) -> Result<(), InstructionError> { + if let StakeState::Uninitialized = split.state()? { + // verify enough account lamports + if lamports > self.account.lamports { + return Err(InstructionError::InsufficientFunds); + } + + match self.state()? { + StakeState::Stake(meta, mut stake) => { + meta.authorized.check(signers, StakeAuthorize::Staker)?; + + // verify enough lamports for rent in new stake with the split + if split.account.lamports + lamports < meta.rent_exempt_reserve + // verify enough lamports left in previous stake + || lamports + meta.rent_exempt_reserve > self.account.lamports + { + return Err(InstructionError::InsufficientFunds); + } + + // split the stake, subtract rent_exempt_balance unless + // the destination account already has those lamports + // in place. + // this could represent a small loss of staked lamports + // if the split account starts out with a zero balance + let split_stake = stake.split( + lamports + - meta + .rent_exempt_reserve + .saturating_sub(split.account.lamports), + )?; + + self.set_state(&StakeState::Stake(meta, stake))?; + split.set_state(&StakeState::Stake(meta, split_stake))?; + } + StakeState::Initialized(meta) => { + meta.authorized.check(signers, StakeAuthorize::Staker)?; + + // enough lamports for rent in new stake + if lamports < meta.rent_exempt_reserve + // verify enough lamports left in previous stake + || lamports + meta.rent_exempt_reserve > self.account.lamports + { + return Err(InstructionError::InsufficientFunds); + } + + split.set_state(&StakeState::Initialized(meta))?; + } + StakeState::Uninitialized => { + if !signers.contains(&self.unsigned_key()) { + return Err(InstructionError::MissingRequiredSignature); + } + } + _ => return Err(InstructionError::InvalidAccountData), + } + + split.account.lamports += lamports; + self.account.lamports -= lamports; + Ok(()) + } else { + Err(InstructionError::InvalidAccountData) + } + } + fn withdraw( &mut self, lamports: u64, @@ -570,9 +694,9 @@ impl<'a> StakeAccount for KeyedAccount<'a> { stake_history: &sysvar::stake_history::StakeHistory, signers: &HashSet, ) -> Result<(), InstructionError> { - let lockup = match self.state()? { - StakeState::Stake(authorized, lockup, stake) => { - authorized.check(signers, StakeAuthorize::Withdrawer)?; + let (lockup, reserve, is_staked) = match self.state()? { + StakeState::Stake(meta, stake) => { + meta.authorized.check(signers, StakeAuthorize::Withdrawer)?; // if we have a deactivation epoch and we're in cooldown let staked = if clock.epoch >= stake.deactivation_epoch { stake.stake(clock.epoch, Some(stake_history)) @@ -583,28 +707,39 @@ impl<'a> StakeAccount for KeyedAccount<'a> { stake.stake }; - if lamports > self.account.lamports.saturating_sub(staked) { - return Err(InstructionError::InsufficientFunds); - } - lockup + (meta.lockup, staked + meta.rent_exempt_reserve, staked != 0) } - StakeState::Initialized(authorized, lockup) => { - authorized.check(signers, StakeAuthorize::Withdrawer)?; - lockup + StakeState::Initialized(meta) => { + meta.authorized.check(signers, StakeAuthorize::Withdrawer)?; + + (meta.lockup, meta.rent_exempt_reserve, false) } StakeState::Uninitialized => { - if self.signer_key().is_none() { + if !signers.contains(&self.unsigned_key()) { return Err(InstructionError::MissingRequiredSignature); } - Lockup::default() // no lockup + (Lockup::default(), 0, false) // no lockup, no restrictions } _ => return Err(InstructionError::InvalidAccountData), }; + // verify that lockup has expired or that the withdrawal is going back + // to the custodian if lockup.slot > clock.slot && lockup.custodian != *to.unsigned_key() { return Err(StakeError::LockupInForce.into()); } - if lamports > self.account.lamports { + + // if the stake is active, we mustn't allow the account to go away + if is_staked // line coverage for branch coverage + && lamports + reserve > self.account.lamports + { + return Err(InstructionError::InsufficientFunds); + } + + if lamports != self.account.lamports // not a full withdrawal + && lamports + reserve > self.account.lamports + { + assert!(!is_staked); return Err(InstructionError::InsufficientFunds); } @@ -652,11 +787,15 @@ pub fn create_account( stake_account .set_state(&StakeState::Stake( - Authorized { - staker: *authorized, - withdrawer: *authorized, + Meta { + rent_exempt_reserve: Rent::default() + .minimum_balance(std::mem::size_of::()), + authorized: Authorized { + staker: *authorized, + withdrawer: *authorized, + }, + lockup: Lockup::default(), }, - Lockup::default(), Stake::new_bootstrap(lamports, voter_pubkey, &vote_state), )) .expect("set_state"); @@ -724,13 +863,13 @@ mod tests { let stake_lamports = 42; let mut stake_account = Account::new_data_with_space( stake_lamports, - &StakeState::Initialized( - Authorized { + &StakeState::Initialized(Meta { + authorized: Authorized { staker: stake_pubkey, withdrawer: stake_pubkey, }, - Lockup::default(), - ), + ..Meta::default() + }), std::mem::size_of::(), &id(), ) @@ -743,13 +882,13 @@ mod tests { let stake_state: StakeState = stake_keyed_account.state().unwrap(); assert_eq!( stake_state, - StakeState::Initialized( - Authorized { + StakeState::Initialized(Meta { + authorized: Authorized { staker: stake_pubkey, withdrawer: stake_pubkey, }, - Lockup::default(), - ) + ..Meta::default() + }) ); } @@ -1162,7 +1301,7 @@ mod tests { } #[test] - fn test_stake_lockup() { + fn test_stake_initialize() { let stake_pubkey = Pubkey::new_rand(); let stake_lamports = 42; let mut stake_account = @@ -1171,32 +1310,45 @@ mod tests { // unsigned keyed account let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &mut stake_account); let custodian = Pubkey::new_rand(); + + // not enough balance for rent... assert_eq!( stake_keyed_account.initialize( - &Authorized { - staker: stake_pubkey, - withdrawer: stake_pubkey - }, - &Lockup { slot: 1, custodian } + &Authorized::default(), + &Lockup::default(), + &Rent { + lamports_per_byte_year: 42, + ..Rent::default() + } + ), + Err(InstructionError::InsufficientFunds) + ); + + // this one works, as is uninit + assert_eq!( + stake_keyed_account.initialize( + &Authorized::auto(&stake_pubkey), + &Lockup { slot: 1, custodian }, + &Rent::default(), ), Ok(()) ); - - // first time works, as is uninit + // check that we see what we expect assert_eq!( StakeState::from(&stake_keyed_account.account).unwrap(), - StakeState::Initialized( - Authorized { - staker: stake_pubkey, - withdrawer: stake_pubkey - }, - Lockup { slot: 1, custodian } - ) + StakeState::Initialized(Meta { + lockup: Lockup { slot: 1, custodian }, + ..Meta::auto(&stake_pubkey) + }) ); - // 2nd time fails, can't move it from anything other than uninit->lockup + // 2nd time fails, can't move it from anything other than uninit->init assert_eq!( - stake_keyed_account.initialize(&Authorized::default(), &Lockup::default()), + stake_keyed_account.initialize( + &Authorized::default(), + &Lockup::default(), + &Rent::default() + ), Err(InstructionError::InvalidAccountData) ); } @@ -1207,7 +1359,7 @@ mod tests { let stake_lamports = 42; let mut stake_account = Account::new_data_with_space( stake_lamports, - &StakeState::Initialized(Authorized::auto(&stake_pubkey), Lockup::default()), + &StakeState::Initialized(Meta::auto(&stake_pubkey)), std::mem::size_of::(), &id(), ) @@ -1320,6 +1472,7 @@ mod tests { .initialize( &Authorized::auto(&stake_pubkey), &Lockup { slot: 0, custodian }, + &Rent::default(), ) .unwrap(); @@ -1428,7 +1581,7 @@ mod tests { let stake_lamports = 42; let mut stake_account = Account::new_data_with_space( total_lamports, - &StakeState::Initialized(Authorized::auto(&stake_pubkey), Lockup::default()), + &StakeState::Initialized(Meta::auto(&stake_pubkey)), std::mem::size_of::(), &id(), ) @@ -1516,10 +1669,10 @@ mod tests { let total_lamports = 100; let mut stake_account = Account::new_data_with_space( total_lamports, - &StakeState::Initialized( - Authorized::auto(&stake_pubkey), - Lockup { slot: 1, custodian }, - ), + &StakeState::Initialized(Meta { + lockup: Lockup { slot: 1, custodian }, + ..Meta::auto(&stake_pubkey) + }), std::mem::size_of::(), &id(), ) @@ -1674,7 +1827,7 @@ mod tests { let stake_lamports = 100; let mut stake_account = Account::new_data_with_space( stake_lamports, - &StakeState::Initialized(Authorized::auto(&stake_pubkey), Lockup::default()), + &StakeState::Initialized(Meta::auto(&stake_pubkey)), std::mem::size_of::(), &id(), ) @@ -1799,13 +1952,33 @@ mod tests { ); } + #[test] + fn test_authorize_uninit() { + let stake_pubkey = Pubkey::new_rand(); + let stake_lamports = 42; + let mut stake_account = Account::new_data_with_space( + stake_lamports, + &StakeState::default(), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); + let signers = vec![stake_pubkey].into_iter().collect(); + assert_eq!( + stake_keyed_account.authorize(&stake_pubkey, StakeAuthorize::Staker, &signers), + Err(InstructionError::InvalidAccountData) + ); + } + #[test] fn test_authorize_lockup() { let stake_pubkey = Pubkey::new_rand(); let stake_lamports = 42; let mut stake_account = Account::new_data_with_space( stake_lamports, - &StakeState::Initialized(Authorized::auto(&stake_pubkey), Lockup::default()), + &StakeState::Initialized(Meta::auto(&stake_pubkey)), std::mem::size_of::(), &id(), ) @@ -1828,7 +2001,7 @@ mod tests { stake_keyed_account.authorize(&stake_pubkey0, StakeAuthorize::Withdrawer, &signers), Ok(()) ); - if let StakeState::Initialized(authorized, _lockup) = + if let StakeState::Initialized(Meta { authorized, .. }) = StakeState::from(&stake_keyed_account.account).unwrap() { assert_eq!(authorized.staker, stake_pubkey0); @@ -1852,7 +2025,7 @@ mod tests { stake_keyed_account.authorize(&stake_pubkey2, StakeAuthorize::Staker, &signers0), Ok(()) ); - if let StakeState::Initialized(authorized, _lockup) = + if let StakeState::Initialized(Meta { authorized, .. }) = StakeState::from(&stake_keyed_account.account).unwrap() { assert_eq!(authorized.staker, stake_pubkey2); @@ -1862,7 +2035,7 @@ mod tests { stake_keyed_account.authorize(&stake_pubkey2, StakeAuthorize::Withdrawer, &signers0,), Ok(()) ); - if let StakeState::Initialized(authorized, _lockup) = + if let StakeState::Initialized(Meta { authorized, .. }) = StakeState::from(&stake_keyed_account.account).unwrap() { assert_eq!(authorized.staker, stake_pubkey2); @@ -1896,13 +2069,337 @@ mod tests { ); } + #[test] + fn test_split_source_uninitialized() { + let stake_pubkey = Pubkey::new_rand(); + let stake_lamports = 42; + let mut stake_account = Account::new_data_with_space( + stake_lamports, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let split_stake_pubkey = Pubkey::new_rand(); + let mut split_stake_account = Account::new_data_with_space( + 0, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &mut stake_account); + let mut split_stake_keyed_account = + KeyedAccount::new(&split_stake_pubkey, false, &mut split_stake_account); + + // no signers should fail + assert_eq!( + stake_keyed_account.split( + stake_lamports / 2, + &mut split_stake_keyed_account, + &HashSet::default() // no signers + ), + Err(InstructionError::MissingRequiredSignature) + ); + + // this should work + let signers = vec![stake_pubkey].into_iter().collect(); + assert_eq!( + stake_keyed_account.split(stake_lamports / 2, &mut split_stake_keyed_account, &signers), + Ok(()) + ); + assert_eq!( + stake_keyed_account.account.lamports, + split_stake_keyed_account.account.lamports + ); + } + + #[test] + fn test_split_split_not_uninitialized() { + let stake_pubkey = Pubkey::new_rand(); + let stake_lamports = 42; + let mut stake_account = Account::new_data_with_space( + stake_lamports, + &StakeState::Stake( + Meta::auto(&stake_pubkey), + Stake { + stake: stake_lamports, + ..Stake::default() + }, + ), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let split_stake_pubkey = Pubkey::new_rand(); + let mut split_stake_account = Account::new_data_with_space( + 0, + &StakeState::Initialized(Meta::auto(&stake_pubkey)), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let signers = vec![stake_pubkey].into_iter().collect(); + let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); + let mut split_stake_keyed_account = + KeyedAccount::new(&split_stake_pubkey, true, &mut split_stake_account); + assert_eq!( + stake_keyed_account.split(stake_lamports / 2, &mut split_stake_keyed_account, &signers), + Err(InstructionError::InvalidAccountData) + ); + } + + #[test] + fn test_split_more_than_staked() { + let stake_pubkey = Pubkey::new_rand(); + let stake_lamports = 42; + let mut stake_account = Account::new_data_with_space( + stake_lamports, + &StakeState::Stake( + Meta::auto(&stake_pubkey), + Stake { + stake: stake_lamports / 2 - 1, + ..Stake::default() + }, + ), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let split_stake_pubkey = Pubkey::new_rand(); + let mut split_stake_account = Account::new_data_with_space( + 0, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let signers = vec![stake_pubkey].into_iter().collect(); + let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account); + let mut split_stake_keyed_account = + KeyedAccount::new(&split_stake_pubkey, true, &mut split_stake_account); + assert_eq!( + stake_keyed_account.split(stake_lamports / 2, &mut split_stake_keyed_account, &signers), + Err(StakeError::InsufficientStake.into()) + ); + } + + #[test] + fn test_split_with_rent() { + let stake_pubkey = Pubkey::new_rand(); + let split_stake_pubkey = Pubkey::new_rand(); + let stake_lamports = 42; + let rent_exempt_reserve = 10; + let signers = vec![stake_pubkey].into_iter().collect(); + + let meta = Meta { + authorized: Authorized::auto(&stake_pubkey), + rent_exempt_reserve, + ..Meta::default() + }; + + // test splitting both an Initialized stake and a Staked stake + for state in &[ + StakeState::Initialized(meta), + StakeState::Stake( + meta, + Stake { + stake: stake_lamports, + ..Stake::default() + }, + ), + ] { + let mut stake_account = Account::new_data_with_space( + stake_lamports, + state, + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let mut stake_keyed_account = + KeyedAccount::new(&stake_pubkey, true, &mut stake_account); + + let mut split_stake_account = Account::new_data_with_space( + 0, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let mut split_stake_keyed_account = + KeyedAccount::new(&split_stake_pubkey, true, &mut split_stake_account); + + // not enough to make a stake account + assert_eq!( + stake_keyed_account.split( + rent_exempt_reserve - 1, + &mut split_stake_keyed_account, + &signers + ), + Err(InstructionError::InsufficientFunds) + ); + + // doesn't leave enough for initial stake + assert_eq!( + stake_keyed_account.split( + (stake_lamports - rent_exempt_reserve) + 1, + &mut split_stake_keyed_account, + &signers + ), + Err(InstructionError::InsufficientFunds) + ); + + // split account already has way enough lamports + split_stake_keyed_account.account.lamports = 1_000; + assert_eq!( + stake_keyed_account.split( + stake_lamports - rent_exempt_reserve, + &mut split_stake_keyed_account, + &signers + ), + Ok(()) + ); + + // verify no stake leakage in the case of a stake + if let StakeState::Stake(meta, stake) = state { + assert_eq!( + split_stake_keyed_account.state(), + Ok(StakeState::Stake( + *meta, + Stake { + stake: stake_lamports - rent_exempt_reserve, + ..*stake + } + )) + ); + // assert_eq!( + // stake_keyed_account.state(), + // Ok(StakeState::Stake(*meta, Stake { stake: 0, ..*stake })) + // ); + assert_eq!(stake_keyed_account.account.lamports, rent_exempt_reserve); + assert_eq!( + split_stake_keyed_account.account.lamports, + 1_000 + stake_lamports - rent_exempt_reserve + ); + } + } + } + + #[test] + fn test_split() { + let stake_pubkey = Pubkey::new_rand(); + let stake_lamports = 42; + + let split_stake_pubkey = Pubkey::new_rand(); + let signers = vec![stake_pubkey].into_iter().collect(); + + // test splitting both an Initialized stake and a Staked stake + for state in &[ + StakeState::Initialized(Meta::auto(&stake_pubkey)), + StakeState::Stake( + Meta::auto(&stake_pubkey), + Stake { + stake: stake_lamports, + ..Stake::default() + }, + ), + ] { + let mut split_stake_account = Account::new_data_with_space( + 0, + &StakeState::Uninitialized, + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let mut split_stake_keyed_account = + KeyedAccount::new(&split_stake_pubkey, true, &mut split_stake_account); + + let mut stake_account = Account::new_data_with_space( + stake_lamports, + state, + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + let mut stake_keyed_account = + KeyedAccount::new(&stake_pubkey, true, &mut stake_account); + + // split more than available fails + assert_eq!( + stake_keyed_account.split( + stake_lamports + 1, + &mut split_stake_keyed_account, + &signers + ), + Err(InstructionError::InsufficientFunds) + ); + + // should work + assert_eq!( + stake_keyed_account.split( + stake_lamports / 2, + &mut split_stake_keyed_account, + &signers + ), + Ok(()) + ); + // no lamport leakage + assert_eq!( + stake_keyed_account.account.lamports + split_stake_keyed_account.account.lamports, + stake_lamports + ); + + match state { + StakeState::Initialized(_) => { + assert_eq!(Ok(*state), split_stake_keyed_account.state()); + assert_eq!(Ok(*state), stake_keyed_account.state()); + } + StakeState::Stake(meta, stake) => { + assert_eq!( + Ok(StakeState::Stake( + *meta, + Stake { + stake: stake_lamports / 2, + ..*stake + } + )), + split_stake_keyed_account.state() + ); + assert_eq!( + Ok(StakeState::Stake( + *meta, + Stake { + stake: stake_lamports / 2, + ..*stake + } + )), + stake_keyed_account.state() + ); + } + _ => unreachable!(), + } + + // reset + stake_keyed_account.account.lamports = stake_lamports; + } + } + #[test] fn test_authorize_delegated_stake() { let stake_pubkey = Pubkey::new_rand(); let stake_lamports = 42; let mut stake_account = Account::new_data_with_space( stake_lamports, - &StakeState::Initialized(Authorized::auto(&stake_pubkey), Lockup::default()), + &StakeState::Initialized(Meta::auto(&stake_pubkey)), std::mem::size_of::(), &id(), ) diff --git a/programs/stake_tests/tests/stake_instruction.rs b/programs/stake_tests/tests/stake_instruction.rs index 22d3d6ab62..9abdf38ccb 100644 --- a/programs/stake_tests/tests/stake_instruction.rs +++ b/programs/stake_tests/tests/stake_instruction.rs @@ -111,7 +111,7 @@ fn test_stake_account_delegate() { // Test that correct lamports are staked let account = bank.get_account(&staker_pubkey).expect("account not found"); let stake_state = account.state().expect("couldn't unpack account data"); - if let StakeState::Stake(_authorized, _lockup, stake) = stake_state { + if let StakeState::Stake(_meta, stake) = stake_state { assert_eq!(stake.stake, 1_000_000); } else { assert!(false, "wrong account type found") @@ -134,7 +134,7 @@ fn test_stake_account_delegate() { // Test that lamports are still staked let account = bank.get_account(&staker_pubkey).expect("account not found"); let stake_state = account.state().expect("couldn't unpack account data"); - if let StakeState::Stake(_authorized, _lockup, stake) = stake_state { + if let StakeState::Stake(_meta, stake) = stake_state { assert_eq!(stake.stake, 1_000_000); } else { assert!(false, "wrong account type found") diff --git a/sdk/src/clock.rs b/sdk/src/clock.rs index 5089b956dc..c6bc557697 100644 --- a/sdk/src/clock.rs +++ b/sdk/src/clock.rs @@ -72,6 +72,15 @@ pub type Segment = u64; /// some number of Slots. pub type Epoch = u64; +#[repr(C)] +#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] +pub struct Clock { + pub slot: Slot, + pub segment: Segment, + pub epoch: Epoch, + pub stakers_epoch: Epoch, +} + #[cfg(test)] mod tests { use super::*; diff --git a/sdk/src/sysvar/clock.rs b/sdk/src/sysvar/clock.rs index 4e43ac8ea2..e28db4841b 100644 --- a/sdk/src/sysvar/clock.rs +++ b/sdk/src/sysvar/clock.rs @@ -1,10 +1,13 @@ //! This account contains the clock slot, epoch, and stakers_epoch //! +pub use crate::clock::Clock; -use crate::account::Account; -use crate::account_info::AccountInfo; -use crate::clock::{Epoch, Segment, Slot}; -use crate::sysvar; +use crate::{ + account::Account, + account_info::AccountInfo, + clock::{Epoch, Segment, Slot}, + sysvar, +}; use bincode::serialized_size; const ID: [u8; 32] = [ @@ -14,15 +17,6 @@ const ID: [u8; 32] = [ crate::solana_sysvar_id!(ID, "SysvarC1ock11111111111111111111111111111111"); -#[repr(C)] -#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] -pub struct Clock { - pub slot: Slot, - pub segment: Segment, - pub epoch: Epoch, - pub stakers_epoch: Epoch, -} - impl Clock { pub fn size_of() -> usize { serialized_size(&Self::default()).unwrap() as usize