stake split (#6402)

* stake split

* stake split
This commit is contained in:
Rob Walker 2019-10-31 11:07:27 -07:00 committed by GitHub
parent 3a616de47b
commit bc88180058
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 770 additions and 125 deletions

View File

@ -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);

View File

@ -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<E> DecodeError<E> 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::<StakeState>() as u64,
&id(),
),
initialize(stake_pubkey, authorized, lockup),
]
}
pub fn split(
stake_pubkey: &Pubkey,
authorized_pubkey: &Pubkey,
lamports: u64,
split_stake_pubkey: &Pubkey,
) -> Vec<Instruction> {
vec![
system_instruction::create_account(
stake_pubkey,
split_stake_pubkey,
0, // creates an ephemeral, uninitialized Stake
std::mem::size_of::<StakeState>() 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(

View File

@ -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<Stake> {
match self {
StakeState::Stake(_authorized, _lockup, stake) => Some(*stake),
StakeState::Stake(_meta, stake) => Some(*stake),
_ => None,
}
}
pub fn authorized(&self) -> Option<Authorized> {
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::<StakeState>()),
..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<Self, StakeError> {
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<Pubkey>,
) -> 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<Pubkey>,
) -> 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<Pubkey>,
) -> 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<Pubkey>,
) -> 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<Pubkey>,
) -> 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<Pubkey>,
) -> 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::<StakeState>()),
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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&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::<StakeState>(),
&id(),
)

View File

@ -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")

View File

@ -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::*;

View File

@ -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