diff --git a/book/src/cluster/stake-delegation-and-rewards.md b/book/src/cluster/stake-delegation-and-rewards.md index 91e68eef4b..0d88c7bcf3 100644 --- a/book/src/cluster/stake-delegation-and-rewards.md +++ b/book/src/cluster/stake-delegation-and-rewards.md @@ -103,7 +103,7 @@ The Stake account is moved from Ininitialized to StakeState::Stake form. This is ### StakeInstruction::Authorize\(Pubkey, StakeAuthorize\) -Updates the account with a new authorized staker or withdrawer, according to the StakeAuthorize parameter \(`Staker` or `Withdrawer`\). The transaction must be by signed by the Stakee account's current `authorized_staker` or `authorized_withdrawer`. +Updates the account with a new authorized staker or withdrawer, according to the StakeAuthorize parameter \(`Staker` or `Withdrawer`\). The transaction must be by signed by the Stakee account's current `authorized_staker` or `authorized_withdrawer`. Any stake lock-up must have expired, or the lock-up custodian must also sign the transaction. * `account[0]` - RW - The StakeState @@ -228,4 +228,4 @@ Only lamports in excess of effective+activating stake may be withdrawn at any ti ### Lock-up -Stake accounts support the notion of lock-up, wherein the stake account balance is unavailable for withdrawal until a specified time. Lock-up is specified as an epoch height, i.e. the minimum epoch height that must be reached by the network before the stake account balance is available for withdrawal, unless the transaction is also signed by a specified custodian. This information is gathered when the stake account is created, and stored in the Lockup field of the stake account's state. +Stake accounts support the notion of lock-up, wherein the stake account balance is unavailable for withdrawal until a specified time. Lock-up is specified as an epoch height, i.e. the minimum epoch height that must be reached by the network before the stake account balance is available for withdrawal, unless the transaction is also signed by a specified custodian. This information is gathered when the stake account is created, and stored in the Lockup field of the stake account's state. Changing the authorized staker or withdrawer is also subject to lock-up, as such an operation is effectively a transfer. diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 7b0e0aa749..f240b92ce7 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -256,7 +256,11 @@ pub fn authorize( new_authorized_pubkey: &Pubkey, stake_authorize: StakeAuthorize, ) -> Instruction { - let account_metas = vec![AccountMeta::new(*stake_pubkey, false)].with_signer(authorized_pubkey); + let account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ] + .with_signer(authorized_pubkey); Instruction::new( id(), @@ -357,9 +361,12 @@ pub fn process_instruction( &lockup, &Rent::from_keyed_account(next_keyed_account(keyed_accounts)?)?, ), - StakeInstruction::Authorize(authorized_pubkey, stake_authorize) => { - me.authorize(&authorized_pubkey, stake_authorize, &signers) - } + StakeInstruction::Authorize(authorized_pubkey, stake_authorize) => me.authorize( + &authorized_pubkey, + stake_authorize, + &signers, + &Clock::from_keyed_account(next_keyed_account(keyed_accounts)?)?, + ), StakeInstruction::DelegateStake => { let vote = next_keyed_account(keyed_accounts)?; diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 5638470cec..f346c5980c 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -118,6 +118,24 @@ pub struct Meta { pub lockup: Lockup, } +impl Meta { + pub fn authorize( + &mut self, + authority: &Pubkey, + stake_authorize: StakeAuthorize, + signers: &HashSet, + clock: &Clock, + ) -> Result<(), InstructionError> { + // verify that lockup has expired or that the authorization + // is *also* signed by the custodian + if self.lockup.is_in_force(clock, signers) { + return Err(StakeError::LockupInForce.into()); + } + self.authorized + .authorize(signers, authority, stake_authorize) + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] pub struct Delegation { /// to whom the stake is delegated @@ -488,6 +506,7 @@ pub trait StakeAccount { authority: &Pubkey, stake_authorize: StakeAuthorize, signers: &HashSet, + clock: &Clock, ) -> Result<(), InstructionError>; fn delegate_stake( &mut self, @@ -556,16 +575,15 @@ impl<'a> StakeAccount for KeyedAccount<'a> { authority: &Pubkey, stake_authorize: StakeAuthorize, signers: &HashSet, + clock: &Clock, ) -> Result<(), InstructionError> { match self.state()? { StakeState::Stake(mut meta, stake) => { - meta.authorized - .authorize(signers, authority, stake_authorize)?; + meta.authorize(authority, stake_authorize, signers, clock)?; self.set_state(&StakeState::Stake(meta, stake)) } StakeState::Initialized(mut meta) => { - meta.authorized - .authorize(signers, authority, stake_authorize)?; + meta.authorize(authority, stake_authorize, signers, clock)?; self.set_state(&StakeState::Initialized(meta)) } _ => Err(InstructionError::InvalidAccountData), @@ -888,6 +906,53 @@ mod tests { } } + #[test] + fn test_meta_authorize() { + let staker = Pubkey::new_rand(); + let custodian = Pubkey::new_rand(); + let mut meta = Meta { + authorized: Authorized::auto(&staker), + lockup: Lockup { + epoch: 0, + unix_timestamp: 0, + custodian, + }, + ..Meta::default() + }; + // verify sig check + let mut signers = HashSet::new(); + let mut clock = Clock::default(); + + assert_eq!( + meta.authorize(&staker, StakeAuthorize::Staker, &signers, &clock), + Err(InstructionError::MissingRequiredSignature) + ); + signers.insert(staker); + assert_eq!( + meta.authorize(&staker, StakeAuthorize::Staker, &signers, &clock), + Ok(()) + ); + // verify lockup check + meta.lockup.epoch = 1; + assert_eq!( + meta.authorize(&staker, StakeAuthorize::Staker, &signers, &clock), + Err(StakeError::LockupInForce.into()) + ); + // verify lockup check defeated by custodian + signers.insert(custodian); + assert_eq!( + meta.authorize(&staker, StakeAuthorize::Staker, &signers, &clock), + Ok(()) + ); + // verify lock expiry + signers.remove(&custodian); + clock.epoch = 1; + assert_eq!( + meta.authorize(&staker, StakeAuthorize::Staker, &signers, &clock), + Ok(()) + ); + } + #[test] fn test_stake_state_stake_from_fail() { let mut stake_account = Account::new(0, std::mem::size_of::(), &id()); @@ -2085,7 +2150,12 @@ mod tests { 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), + stake_keyed_account.authorize( + &stake_pubkey, + StakeAuthorize::Staker, + &signers, + &Clock::default() + ), Err(InstructionError::InvalidAccountData) ); } @@ -2112,11 +2182,21 @@ mod tests { let stake_pubkey0 = Pubkey::new_rand(); let signers = vec![stake_pubkey].into_iter().collect(); assert_eq!( - stake_keyed_account.authorize(&stake_pubkey0, StakeAuthorize::Staker, &signers), + stake_keyed_account.authorize( + &stake_pubkey0, + StakeAuthorize::Staker, + &signers, + &Clock::default() + ), Ok(()) ); assert_eq!( - stake_keyed_account.authorize(&stake_pubkey0, StakeAuthorize::Withdrawer, &signers), + stake_keyed_account.authorize( + &stake_pubkey0, + StakeAuthorize::Withdrawer, + &signers, + &Clock::default() + ), Ok(()) ); if let StakeState::Initialized(Meta { authorized, .. }) = @@ -2131,7 +2211,12 @@ mod tests { // A second authorization signed by the stake_keyed_account should fail let stake_pubkey1 = Pubkey::new_rand(); assert_eq!( - stake_keyed_account.authorize(&stake_pubkey1, StakeAuthorize::Staker, &signers), + stake_keyed_account.authorize( + &stake_pubkey1, + StakeAuthorize::Staker, + &signers, + &Clock::default() + ), Err(InstructionError::MissingRequiredSignature) ); @@ -2140,7 +2225,12 @@ mod tests { // Test a second authorization by the newly authorized pubkey let stake_pubkey2 = Pubkey::new_rand(); assert_eq!( - stake_keyed_account.authorize(&stake_pubkey2, StakeAuthorize::Staker, &signers0), + stake_keyed_account.authorize( + &stake_pubkey2, + StakeAuthorize::Staker, + &signers0, + &Clock::default() + ), Ok(()) ); if let StakeState::Initialized(Meta { authorized, .. }) = @@ -2150,7 +2240,12 @@ mod tests { } assert_eq!( - stake_keyed_account.authorize(&stake_pubkey2, StakeAuthorize::Withdrawer, &signers0,), + stake_keyed_account.authorize( + &stake_pubkey2, + StakeAuthorize::Withdrawer, + &signers0, + &Clock::default() + ), Ok(()) ); if let StakeState::Initialized(Meta { authorized, .. }) = @@ -2689,7 +2784,7 @@ mod tests { ) .expect("stake_account"); - let mut clock = sysvar::clock::Clock::default(); + let mut clock = Clock::default(); let vote_pubkey = Pubkey::new_rand(); let mut vote_account = @@ -2704,7 +2799,12 @@ mod tests { let new_staker_pubkey = Pubkey::new_rand(); assert_eq!( - stake_keyed_account.authorize(&new_staker_pubkey, StakeAuthorize::Staker, &signers), + stake_keyed_account.authorize( + &new_staker_pubkey, + StakeAuthorize::Staker, + &signers, + &clock, + ), Ok(()) ); let authorized = StakeState::authorized_from(&stake_keyed_account.account).unwrap();