diff --git a/interface/src/config.rs b/interface/src/config.rs new file mode 100644 index 00000000..6b7ddfc1 --- /dev/null +++ b/interface/src/config.rs @@ -0,0 +1,33 @@ +//! config for staking +//! carries variables that the stake program cares about + +#[deprecated( + since = "1.16.7", + note = "Please use `solana_sdk::stake::state::{DEFAULT_SLASH_PENALTY, DEFAULT_WARMUP_COOLDOWN_RATE}` instead" +)] +pub use super::state::{DEFAULT_SLASH_PENALTY, DEFAULT_WARMUP_COOLDOWN_RATE}; +use serde_derive::{Deserialize, Serialize}; + +// stake config ID +crate::declare_deprecated_id!("StakeConfig11111111111111111111111111111111"); + +#[deprecated( + since = "1.16.7", + note = "Please use `solana_sdk::stake::state::warmup_cooldown_rate()` instead" +)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] +pub struct Config { + /// how much stake we can activate/deactivate per-epoch as a fraction of currently effective stake + pub warmup_cooldown_rate: f64, + /// percentage of stake lost when slash, expressed as a portion of u8::MAX + pub slash_penalty: u8, +} + +impl Default for Config { + fn default() -> Self { + Self { + warmup_cooldown_rate: DEFAULT_WARMUP_COOLDOWN_RATE, + slash_penalty: DEFAULT_SLASH_PENALTY, + } + } +} diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs new file mode 100644 index 00000000..fff1811b --- /dev/null +++ b/interface/src/instruction.rs @@ -0,0 +1,954 @@ +// Remove the following `allow` when the `Redelegate` variant is renamed to +// `Unused` starting from v3. +// Required to avoid warnings from uses of deprecated types during trait derivations. +#![allow(deprecated)] + +use { + crate::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + stake::{ + config, + program::id, + state::{Authorized, Lockup, StakeAuthorize, StakeStateV2}, + }, + system_instruction, sysvar, + }, + log::*, + num_derive::{FromPrimitive, ToPrimitive}, + serde_derive::{Deserialize, Serialize}, + solana_clock::{Epoch, UnixTimestamp}, + solana_decode_error::DecodeError, + thiserror::Error, +}; + +/// Reasons the stake might have had an error +#[derive(Error, Debug, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)] +pub enum StakeError { + // 0 + #[error("not enough credits to redeem")] + NoCreditsToRedeem, + + #[error("lockup has not yet expired")] + LockupInForce, + + #[error("stake already deactivated")] + AlreadyDeactivated, + + #[error("one re-delegation permitted per epoch")] + TooSoonToRedelegate, + + #[error("split amount is more than is staked")] + InsufficientStake, + + // 5 + #[error("stake account with transient stake cannot be merged")] + MergeTransientStake, + + #[error("stake account merge failed due to different authority, lockups or state")] + MergeMismatch, + + #[error("custodian address not present")] + CustodianMissing, + + #[error("custodian signature not present")] + CustodianSignatureMissing, + + #[error("insufficient voting activity in the reference vote account")] + InsufficientReferenceVotes, + + // 10 + #[error("stake account is not delegated to the provided vote account")] + VoteAddressMismatch, + + #[error( + "stake account has not been delinquent for the minimum epochs required for deactivation" + )] + MinimumDelinquentEpochsForDeactivationNotMet, + + #[error("delegation amount is less than the minimum")] + InsufficientDelegation, + + #[error("stake account with transient or inactive stake cannot be redelegated")] + RedelegateTransientOrInactiveStake, + + #[error("stake redelegation to the same vote account is not permitted")] + RedelegateToSameVoteAccount, + + // 15 + #[error("redelegated stake must be fully activated before deactivation")] + RedelegatedStakeMustFullyActivateBeforeDeactivationIsPermitted, + + #[error("stake action is not permitted while the epoch rewards period is active")] + EpochRewardsActive, +} + +impl From for ProgramError { + fn from(e: StakeError) -> Self { + ProgramError::Custom(e as u32) + } +} + +impl DecodeError for StakeError { + fn type_of() -> &'static str { + "StakeError" + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum StakeInstruction { + /// Initialize a stake with lockup and authorization information + /// + /// # Account references + /// 0. `[WRITE]` Uninitialized stake account + /// 1. `[]` Rent sysvar + /// + /// Authorized carries pubkeys that must sign staker transactions + /// and withdrawer transactions. + /// Lockup carries information about withdrawal restrictions + Initialize(Authorized, Lockup), + + /// Authorize a key to manage stake or withdrawal + /// + /// # Account references + /// 0. `[WRITE]` Stake account to be updated + /// 1. `[]` Clock sysvar + /// 2. `[SIGNER]` The stake or withdraw authority + /// 3. Optional: `[SIGNER]` Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration + Authorize(Pubkey, StakeAuthorize), + + /// Delegate a stake to a particular vote account + /// + /// # Account references + /// 0. `[WRITE]` Initialized stake account to be delegated + /// 1. `[]` Vote account to which this stake will be delegated + /// 2. `[]` Clock sysvar + /// 3. `[]` Stake history sysvar that carries stake warmup/cooldown history + /// 4. `[]` Unused account, formerly the stake config + /// 5. `[SIGNER]` Stake authority + /// + /// The entire balance of the staking account is staked. DelegateStake + /// can be called multiple times, but re-delegation is delayed + /// by one epoch + DelegateStake, + + /// Split u64 tokens and stake off a stake account into another stake account. + /// + /// # Account references + /// 0. `[WRITE]` Stake account to be split; must be in the Initialized or Stake state + /// 1. `[WRITE]` Uninitialized stake account that will take the split-off amount + /// 2. `[SIGNER]` Stake authority + Split(u64), + + /// Withdraw unstaked lamports from the stake account + /// + /// # Account references + /// 0. `[WRITE]` Stake account from which to withdraw + /// 1. `[WRITE]` Recipient account + /// 2. `[]` Clock sysvar + /// 3. `[]` Stake history sysvar that carries stake warmup/cooldown history + /// 4. `[SIGNER]` Withdraw authority + /// 5. Optional: `[SIGNER]` Lockup authority, if before lockup expiration + /// + /// The u64 is the portion of the stake account balance to be withdrawn, + /// must be `<= StakeAccount.lamports - staked_lamports`. + Withdraw(u64), + + /// Deactivates the stake in the account + /// + /// # Account references + /// 0. `[WRITE]` Delegated stake account + /// 1. `[]` Clock sysvar + /// 2. `[SIGNER]` Stake authority + Deactivate, + + /// Set stake lockup + /// + /// If a lockup is not active, the withdraw authority may set a new lockup + /// If a lockup is active, the lockup custodian may update the lockup parameters + /// + /// # Account references + /// 0. `[WRITE]` Initialized stake account + /// 1. `[SIGNER]` Lockup authority or withdraw authority + SetLockup(LockupArgs), + + /// Merge two stake accounts. + /// + /// Both accounts must have identical lockup and authority keys. A merge + /// is possible between two stakes in the following states with no additional + /// conditions: + /// + /// * two deactivated stakes + /// * an inactive stake into an activating stake during its activation epoch + /// + /// For the following cases, the voter pubkey and vote credits observed must match: + /// + /// * two activated stakes + /// * two activating accounts that share an activation epoch, during the activation epoch + /// + /// All other combinations of stake states will fail to merge, including all + /// "transient" states, where a stake is activating or deactivating with a + /// non-zero effective stake. + /// + /// # Account references + /// 0. `[WRITE]` Destination stake account for the merge + /// 1. `[WRITE]` Source stake account for to merge. This account will be drained + /// 2. `[]` Clock sysvar + /// 3. `[]` Stake history sysvar that carries stake warmup/cooldown history + /// 4. `[SIGNER]` Stake authority + Merge, + + /// Authorize a key to manage stake or withdrawal with a derived key + /// + /// # Account references + /// 0. `[WRITE]` Stake account to be updated + /// 1. `[SIGNER]` Base key of stake or withdraw authority + /// 2. `[]` Clock sysvar + /// 3. Optional: `[SIGNER]` Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration + AuthorizeWithSeed(AuthorizeWithSeedArgs), + + /// Initialize a stake with authorization information + /// + /// This instruction is similar to `Initialize` except that the withdraw authority + /// must be a signer, and no lockup is applied to the account. + /// + /// # Account references + /// 0. `[WRITE]` Uninitialized stake account + /// 1. `[]` Rent sysvar + /// 2. `[]` The stake authority + /// 3. `[SIGNER]` The withdraw authority + /// + InitializeChecked, + + /// Authorize a key to manage stake or withdrawal + /// + /// This instruction behaves like `Authorize` with the additional requirement that the new + /// stake or withdraw authority must also be a signer. + /// + /// # Account references + /// 0. `[WRITE]` Stake account to be updated + /// 1. `[]` Clock sysvar + /// 2. `[SIGNER]` The stake or withdraw authority + /// 3. `[SIGNER]` The new stake or withdraw authority + /// 4. Optional: `[SIGNER]` Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration + AuthorizeChecked(StakeAuthorize), + + /// Authorize a key to manage stake or withdrawal with a derived key + /// + /// This instruction behaves like `AuthorizeWithSeed` with the additional requirement that + /// the new stake or withdraw authority must also be a signer. + /// + /// # Account references + /// 0. `[WRITE]` Stake account to be updated + /// 1. `[SIGNER]` Base key of stake or withdraw authority + /// 2. `[]` Clock sysvar + /// 3. `[SIGNER]` The new stake or withdraw authority + /// 4. Optional: `[SIGNER]` Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration + AuthorizeCheckedWithSeed(AuthorizeCheckedWithSeedArgs), + + /// Set stake lockup + /// + /// This instruction behaves like `SetLockup` with the additional requirement that + /// the new lockup authority also be a signer. + /// + /// If a lockup is not active, the withdraw authority may set a new lockup + /// If a lockup is active, the lockup custodian may update the lockup parameters + /// + /// # Account references + /// 0. `[WRITE]` Initialized stake account + /// 1. `[SIGNER]` Lockup authority or withdraw authority + /// 2. Optional: `[SIGNER]` New lockup authority + SetLockupChecked(LockupCheckedArgs), + + /// Get the minimum stake delegation, in lamports + /// + /// # Account references + /// None + /// + /// Returns the minimum delegation as a little-endian encoded u64 value. + /// Programs can use the [`get_minimum_delegation()`] helper function to invoke and + /// retrieve the return value for this instruction. + /// + /// [`get_minimum_delegation()`]: super::tools::get_minimum_delegation + GetMinimumDelegation, + + /// Deactivate stake delegated to a vote account that has been delinquent for at least + /// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` epochs. + /// + /// No signer is required for this instruction as it is a common good to deactivate abandoned + /// stake. + /// + /// # Account references + /// 0. `[WRITE]` Delegated stake account + /// 1. `[]` Delinquent vote account for the delegated stake account + /// 2. `[]` Reference vote account that has voted at least once in the last + /// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` epochs + DeactivateDelinquent, + + /// Redelegate activated stake to another vote account. + /// + /// Upon success: + /// * the balance of the delegated stake account will be reduced to the undelegated amount in + /// the account (rent exempt minimum and any additional lamports not part of the delegation), + /// and scheduled for deactivation. + /// * the provided uninitialized stake account will receive the original balance of the + /// delegated stake account, minus the rent exempt minimum, and scheduled for activation to + /// the provided vote account. Any existing lamports in the uninitialized stake account + /// will also be included in the re-delegation. + /// + /// # Account references + /// 0. `[WRITE]` Delegated stake account to be redelegated. The account must be fully + /// activated and carry a balance greater than or equal to the minimum delegation amount + /// plus rent exempt minimum + /// 1. `[WRITE]` Uninitialized stake account that will hold the redelegated stake + /// 2. `[]` Vote account to which this stake will be re-delegated + /// 3. `[]` Unused account, formerly the stake config + /// 4. `[SIGNER]` Stake authority + /// + #[deprecated(since = "2.1.0", note = "Redelegate will not be enabled")] + Redelegate, + + /// Move stake between accounts with the same authorities and lockups, using Staker authority. + /// + /// The source account must be fully active. If its entire delegation is moved, it immediately + /// becomes inactive. Otherwise, at least the minimum delegation of active stake must remain. + /// + /// The destination account must be fully active or fully inactive. If it is active, it must + /// be delegated to the same vote account as the source. If it is inactive, it + /// immediately becomes active, and must contain at least the minimum delegation. The + /// destination must be pre-funded with the rent-exempt reserve. + /// + /// This instruction only affects or moves active stake. Additional unstaked lamports are never + /// moved, activated, or deactivated, and accounts are never deallocated. + /// + /// # Account references + /// 0. `[WRITE]` Active source stake account + /// 1. `[WRITE]` Active or inactive destination stake account + /// 2. `[SIGNER]` Stake authority + /// + /// The u64 is the portion of the stake to move, which may be the entire delegation + MoveStake(u64), + + /// Move unstaked lamports between accounts with the same authorities and lockups, using Staker + /// authority. + /// + /// The source account must be fully active or fully inactive. The destination may be in any + /// mergeable state (active, inactive, or activating, but not in warmup cooldown). Only lamports that + /// are neither backing a delegation nor required for rent-exemption may be moved. + /// + /// # Account references + /// 0. `[WRITE]` Active or inactive source stake account + /// 1. `[WRITE]` Mergeable destination stake account + /// 2. `[SIGNER]` Stake authority + /// + /// The u64 is the portion of available lamports to move + MoveLamports(u64), +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] +pub struct LockupArgs { + pub unix_timestamp: Option, + pub epoch: Option, + pub custodian: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] +pub struct LockupCheckedArgs { + pub unix_timestamp: Option, + pub epoch: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AuthorizeWithSeedArgs { + pub new_authorized_pubkey: Pubkey, + pub stake_authorize: StakeAuthorize, + pub authority_seed: String, + pub authority_owner: Pubkey, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AuthorizeCheckedWithSeedArgs { + pub stake_authorize: StakeAuthorize, + pub authority_seed: String, + pub authority_owner: Pubkey, +} + +pub fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Lockup) -> Instruction { + Instruction::new_with_bincode( + id(), + &StakeInstruction::Initialize(*authorized, *lockup), + vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + ], + ) +} + +pub fn initialize_checked(stake_pubkey: &Pubkey, authorized: &Authorized) -> Instruction { + Instruction::new_with_bincode( + id(), + &StakeInstruction::InitializeChecked, + vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(authorized.staker, false), + AccountMeta::new_readonly(authorized.withdrawer, true), + ], + ) +} + +pub fn create_account_with_seed( + from_pubkey: &Pubkey, + stake_pubkey: &Pubkey, + base: &Pubkey, + seed: &str, + authorized: &Authorized, + lockup: &Lockup, + lamports: u64, +) -> Vec { + vec![ + system_instruction::create_account_with_seed( + from_pubkey, + stake_pubkey, + base, + seed, + lamports, + StakeStateV2::size_of() as u64, + &id(), + ), + initialize(stake_pubkey, authorized, lockup), + ] +} + +pub fn create_account( + from_pubkey: &Pubkey, + stake_pubkey: &Pubkey, + authorized: &Authorized, + lockup: &Lockup, + lamports: u64, +) -> Vec { + vec![ + system_instruction::create_account( + from_pubkey, + stake_pubkey, + lamports, + StakeStateV2::size_of() as u64, + &id(), + ), + initialize(stake_pubkey, authorized, lockup), + ] +} + +pub fn create_account_with_seed_checked( + from_pubkey: &Pubkey, + stake_pubkey: &Pubkey, + base: &Pubkey, + seed: &str, + authorized: &Authorized, + lamports: u64, +) -> Vec { + vec![ + system_instruction::create_account_with_seed( + from_pubkey, + stake_pubkey, + base, + seed, + lamports, + StakeStateV2::size_of() as u64, + &id(), + ), + initialize_checked(stake_pubkey, authorized), + ] +} + +pub fn create_account_checked( + from_pubkey: &Pubkey, + stake_pubkey: &Pubkey, + authorized: &Authorized, + lamports: u64, +) -> Vec { + vec![ + system_instruction::create_account( + from_pubkey, + stake_pubkey, + lamports, + StakeStateV2::size_of() as u64, + &id(), + ), + initialize_checked(stake_pubkey, authorized), + ] +} + +fn _split( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + lamports: u64, + split_stake_pubkey: &Pubkey, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new(*split_stake_pubkey, false), + AccountMeta::new_readonly(*authorized_pubkey, true), + ]; + + Instruction::new_with_bincode(id(), &StakeInstruction::Split(lamports), account_metas) +} + +pub fn split( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + lamports: u64, + split_stake_pubkey: &Pubkey, +) -> Vec { + vec![ + system_instruction::allocate(split_stake_pubkey, StakeStateV2::size_of() as u64), + system_instruction::assign(split_stake_pubkey, &id()), + _split( + stake_pubkey, + authorized_pubkey, + lamports, + split_stake_pubkey, + ), + ] +} + +pub fn split_with_seed( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + lamports: u64, + split_stake_pubkey: &Pubkey, // derived using create_with_seed() + base: &Pubkey, // base + seed: &str, // seed +) -> Vec { + vec![ + system_instruction::allocate_with_seed( + split_stake_pubkey, + base, + seed, + StakeStateV2::size_of() as u64, + &id(), + ), + _split( + stake_pubkey, + authorized_pubkey, + lamports, + split_stake_pubkey, + ), + ] +} + +pub fn merge( + destination_stake_pubkey: &Pubkey, + source_stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, +) -> Vec { + let account_metas = vec![ + AccountMeta::new(*destination_stake_pubkey, false), + AccountMeta::new(*source_stake_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::stake_history::id(), false), + AccountMeta::new_readonly(*authorized_pubkey, true), + ]; + + vec![Instruction::new_with_bincode( + id(), + &StakeInstruction::Merge, + account_metas, + )] +} + +pub fn create_account_and_delegate_stake( + from_pubkey: &Pubkey, + stake_pubkey: &Pubkey, + vote_pubkey: &Pubkey, + authorized: &Authorized, + lockup: &Lockup, + lamports: u64, +) -> Vec { + let mut instructions = create_account(from_pubkey, stake_pubkey, authorized, lockup, lamports); + instructions.push(delegate_stake( + stake_pubkey, + &authorized.staker, + vote_pubkey, + )); + instructions +} + +pub fn create_account_with_seed_and_delegate_stake( + from_pubkey: &Pubkey, + stake_pubkey: &Pubkey, + base: &Pubkey, + seed: &str, + vote_pubkey: &Pubkey, + authorized: &Authorized, + lockup: &Lockup, + lamports: u64, +) -> Vec { + let mut instructions = create_account_with_seed( + from_pubkey, + stake_pubkey, + base, + seed, + authorized, + lockup, + lamports, + ); + instructions.push(delegate_stake( + stake_pubkey, + &authorized.staker, + vote_pubkey, + )); + instructions +} + +pub fn authorize( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + new_authorized_pubkey: &Pubkey, + stake_authorize: StakeAuthorize, + custodian_pubkey: Option<&Pubkey>, +) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*authorized_pubkey, true), + ]; + + if let Some(custodian_pubkey) = custodian_pubkey { + account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true)); + } + + Instruction::new_with_bincode( + id(), + &StakeInstruction::Authorize(*new_authorized_pubkey, stake_authorize), + account_metas, + ) +} + +pub fn authorize_checked( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + new_authorized_pubkey: &Pubkey, + stake_authorize: StakeAuthorize, + custodian_pubkey: Option<&Pubkey>, +) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*authorized_pubkey, true), + AccountMeta::new_readonly(*new_authorized_pubkey, true), + ]; + + if let Some(custodian_pubkey) = custodian_pubkey { + account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true)); + } + + Instruction::new_with_bincode( + id(), + &StakeInstruction::AuthorizeChecked(stake_authorize), + account_metas, + ) +} + +pub fn authorize_with_seed( + stake_pubkey: &Pubkey, + authority_base: &Pubkey, + authority_seed: String, + authority_owner: &Pubkey, + new_authorized_pubkey: &Pubkey, + stake_authorize: StakeAuthorize, + custodian_pubkey: Option<&Pubkey>, +) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(*authority_base, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ]; + + if let Some(custodian_pubkey) = custodian_pubkey { + account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true)); + } + + let args = AuthorizeWithSeedArgs { + new_authorized_pubkey: *new_authorized_pubkey, + stake_authorize, + authority_seed, + authority_owner: *authority_owner, + }; + + Instruction::new_with_bincode( + id(), + &StakeInstruction::AuthorizeWithSeed(args), + account_metas, + ) +} + +pub fn authorize_checked_with_seed( + stake_pubkey: &Pubkey, + authority_base: &Pubkey, + authority_seed: String, + authority_owner: &Pubkey, + new_authorized_pubkey: &Pubkey, + stake_authorize: StakeAuthorize, + custodian_pubkey: Option<&Pubkey>, +) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(*authority_base, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*new_authorized_pubkey, true), + ]; + + if let Some(custodian_pubkey) = custodian_pubkey { + account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true)); + } + + let args = AuthorizeCheckedWithSeedArgs { + stake_authorize, + authority_seed, + authority_owner: *authority_owner, + }; + + Instruction::new_with_bincode( + id(), + &StakeInstruction::AuthorizeCheckedWithSeed(args), + account_metas, + ) +} + +pub fn delegate_stake( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + vote_pubkey: &Pubkey, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(*vote_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::stake_history::id(), false), + // For backwards compatibility we pass the stake config, although this account is unused + AccountMeta::new_readonly(config::id(), false), + AccountMeta::new_readonly(*authorized_pubkey, true), + ]; + Instruction::new_with_bincode(id(), &StakeInstruction::DelegateStake, account_metas) +} + +pub fn withdraw( + stake_pubkey: &Pubkey, + withdrawer_pubkey: &Pubkey, + to_pubkey: &Pubkey, + lamports: u64, + custodian_pubkey: Option<&Pubkey>, +) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new(*to_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::stake_history::id(), false), + AccountMeta::new_readonly(*withdrawer_pubkey, true), + ]; + + if let Some(custodian_pubkey) = custodian_pubkey { + account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true)); + } + + Instruction::new_with_bincode(id(), &StakeInstruction::Withdraw(lamports), account_metas) +} + +pub fn deactivate_stake(stake_pubkey: &Pubkey, authorized_pubkey: &Pubkey) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*authorized_pubkey, true), + ]; + Instruction::new_with_bincode(id(), &StakeInstruction::Deactivate, account_metas) +} + +pub fn set_lockup( + stake_pubkey: &Pubkey, + lockup: &LockupArgs, + custodian_pubkey: &Pubkey, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(*custodian_pubkey, true), + ]; + Instruction::new_with_bincode(id(), &StakeInstruction::SetLockup(*lockup), account_metas) +} + +pub fn set_lockup_checked( + stake_pubkey: &Pubkey, + lockup: &LockupArgs, + custodian_pubkey: &Pubkey, +) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(*custodian_pubkey, true), + ]; + + let lockup_checked = LockupCheckedArgs { + unix_timestamp: lockup.unix_timestamp, + epoch: lockup.epoch, + }; + if let Some(new_custodian) = lockup.custodian { + account_metas.push(AccountMeta::new_readonly(new_custodian, true)); + } + Instruction::new_with_bincode( + id(), + &StakeInstruction::SetLockupChecked(lockup_checked), + account_metas, + ) +} + +pub fn get_minimum_delegation() -> Instruction { + Instruction::new_with_bincode( + id(), + &StakeInstruction::GetMinimumDelegation, + Vec::default(), + ) +} + +pub fn deactivate_delinquent_stake( + stake_account: &Pubkey, + delinquent_vote_account: &Pubkey, + reference_vote_account: &Pubkey, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*stake_account, false), + AccountMeta::new_readonly(*delinquent_vote_account, false), + AccountMeta::new_readonly(*reference_vote_account, false), + ]; + Instruction::new_with_bincode(id(), &StakeInstruction::DeactivateDelinquent, account_metas) +} + +fn _redelegate( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + vote_pubkey: &Pubkey, + uninitialized_stake_pubkey: &Pubkey, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new(*uninitialized_stake_pubkey, false), + AccountMeta::new_readonly(*vote_pubkey, false), + // For backwards compatibility we pass the stake config, although this account is unused + AccountMeta::new_readonly(config::id(), false), + AccountMeta::new_readonly(*authorized_pubkey, true), + ]; + Instruction::new_with_bincode(id(), &StakeInstruction::Redelegate, account_metas) +} + +#[deprecated(since = "2.1.0", note = "Redelegate will not be enabled")] +pub fn redelegate( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + vote_pubkey: &Pubkey, + uninitialized_stake_pubkey: &Pubkey, +) -> Vec { + vec![ + system_instruction::allocate(uninitialized_stake_pubkey, StakeStateV2::size_of() as u64), + system_instruction::assign(uninitialized_stake_pubkey, &id()), + _redelegate( + stake_pubkey, + authorized_pubkey, + vote_pubkey, + uninitialized_stake_pubkey, + ), + ] +} + +#[deprecated(since = "2.1.0", note = "Redelegate will not be enabled")] +pub fn redelegate_with_seed( + stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + vote_pubkey: &Pubkey, + uninitialized_stake_pubkey: &Pubkey, // derived using create_with_seed() + base: &Pubkey, // base + seed: &str, // seed +) -> Vec { + vec![ + system_instruction::allocate_with_seed( + uninitialized_stake_pubkey, + base, + seed, + StakeStateV2::size_of() as u64, + &id(), + ), + _redelegate( + stake_pubkey, + authorized_pubkey, + vote_pubkey, + uninitialized_stake_pubkey, + ), + ] +} + +pub fn move_stake( + source_stake_pubkey: &Pubkey, + destination_stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + lamports: u64, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*source_stake_pubkey, false), + AccountMeta::new(*destination_stake_pubkey, false), + AccountMeta::new_readonly(*authorized_pubkey, true), + ]; + + Instruction::new_with_bincode(id(), &StakeInstruction::MoveStake(lamports), account_metas) +} + +pub fn move_lamports( + source_stake_pubkey: &Pubkey, + destination_stake_pubkey: &Pubkey, + authorized_pubkey: &Pubkey, + lamports: u64, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*source_stake_pubkey, false), + AccountMeta::new(*destination_stake_pubkey, false), + AccountMeta::new_readonly(*authorized_pubkey, true), + ]; + + Instruction::new_with_bincode( + id(), + &StakeInstruction::MoveLamports(lamports), + account_metas, + ) +} + +#[cfg(test)] +mod tests { + use {super::*, crate::instruction::InstructionError}; + + #[test] + fn test_custom_error_decode() { + use num_traits::FromPrimitive; + fn pretty_err(err: InstructionError) -> String + where + T: 'static + std::error::Error + DecodeError + FromPrimitive, + { + if let InstructionError::Custom(code) = err { + let specific_error: T = T::decode_custom_error_to_enum(code).unwrap(); + format!( + "{:?}: {}::{:?} - {}", + err, + T::type_of(), + specific_error, + specific_error, + ) + } else { + "".to_string() + } + } + assert_eq!( + "Custom(0): StakeError::NoCreditsToRedeem - not enough credits to redeem", + pretty_err::(StakeError::NoCreditsToRedeem.into()) + ) + } +} diff --git a/interface/src/lib.rs b/interface/src/lib.rs new file mode 100644 index 00000000..31f13d45 --- /dev/null +++ b/interface/src/lib.rs @@ -0,0 +1,18 @@ +//! The [stake native program][np]. +//! +//! [np]: https://docs.solanalabs.com/runtime/sysvars#stakehistory + +#[allow(deprecated)] +pub mod config; +pub mod instruction; +pub mod stake_flags; +pub mod state; +pub mod tools; + +pub mod program { + crate::declare_id!("Stake11111111111111111111111111111111111111"); +} + +/// The minimum number of epochs before stake account that is delegated to a delinquent vote +/// account may be unstaked with `StakeInstruction::DeactivateDelinquent` +pub const MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION: usize = 5; diff --git a/interface/src/stake_flags.rs b/interface/src/stake_flags.rs new file mode 100644 index 00000000..072305d4 --- /dev/null +++ b/interface/src/stake_flags.rs @@ -0,0 +1,140 @@ +#[cfg(feature = "borsh")] +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; + +/// Additional flags for stake state. +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr( + feature = "borsh", + derive(BorshSerialize, BorshDeserialize, BorshSchema), + borsh(crate = "borsh") +)] +#[derive(Serialize, Deserialize, Copy, PartialEq, Eq, Clone, PartialOrd, Ord, Hash, Debug)] +pub struct StakeFlags { + bits: u8, +} + +#[cfg(feature = "borsh")] +impl borsh0_10::de::BorshDeserialize for StakeFlags { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + bits: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + }) + } +} + +#[cfg(feature = "borsh")] +impl borsh0_10::BorshSchema for StakeFlags { + fn declaration() -> borsh0_10::schema::Declaration { + "StakeFlags".to_string() + } + fn add_definitions_recursively( + definitions: &mut borsh0_10::maybestd::collections::HashMap< + borsh0_10::schema::Declaration, + borsh0_10::schema::Definition, + >, + ) { + let fields = borsh0_10::schema::Fields::NamedFields(<[_]>::into_vec( + borsh0_10::maybestd::boxed::Box::new([( + "bits".to_string(), + ::declaration(), + )]), + )); + let definition = borsh0_10::schema::Definition::Struct { fields }; + Self::add_definition( + ::declaration(), + definition, + definitions, + ); + ::add_definitions_recursively(definitions); + } +} + +#[cfg(feature = "borsh")] +impl borsh0_10::ser::BorshSerialize for StakeFlags { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh0_10::maybestd::io::Error> { + borsh0_10::BorshSerialize::serialize(&self.bits, writer)?; + Ok(()) + } +} + +/// Currently, only bit 1 is used. The other 7 bits are reserved for future usage. +impl StakeFlags { + /// Stake must be fully activated before deactivation is allowed (bit 1). + #[deprecated( + since = "2.1.0", + note = "This flag will be removed because it was only used for `redelegate`, which will not be enabled." + )] + pub const MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED: Self = + Self { bits: 0b0000_0001 }; + + pub const fn empty() -> Self { + Self { bits: 0 } + } + + pub const fn contains(&self, other: Self) -> bool { + (self.bits & other.bits) == other.bits + } + + pub fn remove(&mut self, other: Self) { + self.bits &= !other.bits; + } + + pub fn set(&mut self, other: Self) { + self.bits |= other.bits; + } + + pub const fn union(self, other: Self) -> Self { + Self { + bits: self.bits | other.bits, + } + } +} + +impl Default for StakeFlags { + fn default() -> Self { + StakeFlags::empty() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[allow(deprecated)] + fn test_stake_flags() { + let mut f = StakeFlags::empty(); + assert!(!f.contains(StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED)); + + f.set(StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED); + assert!(f.contains(StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED)); + + f.remove(StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED); + assert!(!f.contains(StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED)); + + let f1 = StakeFlags::empty(); + let f2 = StakeFlags::empty(); + let f3 = f1.union(f2); + assert!(!f3.contains(StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED)); + + let f1 = StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED; + let f2 = StakeFlags::empty(); + let f3 = f1.union(f2); + assert!(f3.contains(StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED)); + + let f1 = StakeFlags::empty(); + let f2 = StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED; + let f3 = f1.union(f2); + assert!(f3.contains(StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED)); + + let f1 = StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED; + let f2 = StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED; + let f3 = f1.union(f2); + assert!(f3.contains(StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED)); + } +} diff --git a/interface/src/stake_history.rs b/interface/src/stake_history.rs new file mode 100644 index 00000000..9438d004 --- /dev/null +++ b/interface/src/stake_history.rs @@ -0,0 +1,127 @@ +//! A type to hold data for the [`StakeHistory` sysvar][sv]. +//! +//! [sv]: https://docs.solanalabs.com/runtime/sysvars#stakehistory +//! +//! The sysvar ID is declared in [`sysvar::stake_history`]. +//! +//! [`sysvar::stake_history`]: crate::sysvar::stake_history + +pub use solana_clock::Epoch; +use std::ops::Deref; + +pub const MAX_ENTRIES: usize = 512; // it should never take as many as 512 epochs to warm up or cool down + +#[repr(C)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default, Clone)] +pub struct StakeHistoryEntry { + pub effective: u64, // effective stake at this epoch + pub activating: u64, // sum of portion of stakes not fully warmed up + pub deactivating: u64, // requested to be cooled down, not fully deactivated yet +} + +impl StakeHistoryEntry { + pub fn with_effective(effective: u64) -> Self { + Self { + effective, + ..Self::default() + } + } + + pub fn with_effective_and_activating(effective: u64, activating: u64) -> Self { + Self { + effective, + activating, + ..Self::default() + } + } + + pub fn with_deactivating(deactivating: u64) -> Self { + Self { + effective: deactivating, + deactivating, + ..Self::default() + } + } +} + +impl std::ops::Add for StakeHistoryEntry { + type Output = StakeHistoryEntry; + fn add(self, rhs: StakeHistoryEntry) -> Self::Output { + Self { + effective: self.effective.saturating_add(rhs.effective), + activating: self.activating.saturating_add(rhs.activating), + deactivating: self.deactivating.saturating_add(rhs.deactivating), + } + } +} + +#[repr(C)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default, Clone)] +pub struct StakeHistory(Vec<(Epoch, StakeHistoryEntry)>); + +impl StakeHistory { + pub fn get(&self, epoch: Epoch) -> Option<&StakeHistoryEntry> { + self.binary_search_by(|probe| epoch.cmp(&probe.0)) + .ok() + .map(|index| &self[index].1) + } + + pub fn add(&mut self, epoch: Epoch, entry: StakeHistoryEntry) { + match self.binary_search_by(|probe| epoch.cmp(&probe.0)) { + Ok(index) => (self.0)[index] = (epoch, entry), + Err(index) => (self.0).insert(index, (epoch, entry)), + } + (self.0).truncate(MAX_ENTRIES); + } +} + +impl Deref for StakeHistory { + type Target = Vec<(Epoch, StakeHistoryEntry)>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub trait StakeHistoryGetEntry { + fn get_entry(&self, epoch: Epoch) -> Option; +} + +impl StakeHistoryGetEntry for StakeHistory { + fn get_entry(&self, epoch: Epoch) -> Option { + self.binary_search_by(|probe| epoch.cmp(&probe.0)) + .ok() + .map(|index| self[index].1.clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_stake_history() { + let mut stake_history = StakeHistory::default(); + + for i in 0..MAX_ENTRIES as u64 + 1 { + stake_history.add( + i, + StakeHistoryEntry { + activating: i, + ..StakeHistoryEntry::default() + }, + ); + } + assert_eq!(stake_history.len(), MAX_ENTRIES); + assert_eq!(stake_history.iter().map(|entry| entry.0).min().unwrap(), 1); + assert_eq!(stake_history.get(0), None); + assert_eq!( + stake_history.get(1), + Some(&StakeHistoryEntry { + activating: 1, + ..StakeHistoryEntry::default() + }) + ); + } +} diff --git a/interface/src/state.rs b/interface/src/state.rs new file mode 100644 index 00000000..4fee1bb0 --- /dev/null +++ b/interface/src/state.rs @@ -0,0 +1,1301 @@ +#![allow(clippy::arithmetic_side_effects)] +#![deny(clippy::wildcard_enum_match_arm)] +// Remove the following `allow` when `StakeState` is removed, required to avoid +// warnings from uses of deprecated types during trait derivations. +#![allow(deprecated)] + +#[cfg(feature = "borsh")] +use borsh::{io, BorshDeserialize, BorshSchema, BorshSerialize}; +use { + crate::{ + instruction::InstructionError, + pubkey::Pubkey, + stake::{ + instruction::{LockupArgs, StakeError}, + stake_flags::StakeFlags, + }, + stake_history::{StakeHistoryEntry, StakeHistoryGetEntry}, + }, + solana_clock::{Clock, Epoch, UnixTimestamp}, + std::collections::HashSet, +}; + +pub type StakeActivationStatus = StakeHistoryEntry; + +// means that no more than RATE of current effective stake may be added or subtracted per +// epoch +pub const DEFAULT_WARMUP_COOLDOWN_RATE: f64 = 0.25; +pub const NEW_WARMUP_COOLDOWN_RATE: f64 = 0.09; +pub const DEFAULT_SLASH_PENALTY: u8 = ((5 * u8::MAX as usize) / 100) as u8; + +pub fn warmup_cooldown_rate(current_epoch: Epoch, new_rate_activation_epoch: Option) -> f64 { + if current_epoch < new_rate_activation_epoch.unwrap_or(u64::MAX) { + DEFAULT_WARMUP_COOLDOWN_RATE + } else { + NEW_WARMUP_COOLDOWN_RATE + } +} + +#[cfg(feature = "borsh")] +macro_rules! impl_borsh_stake_state { + ($borsh:ident) => { + impl $borsh::BorshDeserialize for StakeState { + fn deserialize_reader(reader: &mut R) -> io::Result { + let enum_value: u32 = $borsh::BorshDeserialize::deserialize_reader(reader)?; + match enum_value { + 0 => Ok(StakeState::Uninitialized), + 1 => { + let meta: Meta = $borsh::BorshDeserialize::deserialize_reader(reader)?; + Ok(StakeState::Initialized(meta)) + } + 2 => { + let meta: Meta = $borsh::BorshDeserialize::deserialize_reader(reader)?; + let stake: Stake = $borsh::BorshDeserialize::deserialize_reader(reader)?; + Ok(StakeState::Stake(meta, stake)) + } + 3 => Ok(StakeState::RewardsPool), + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid enum value", + )), + } + } + } + impl $borsh::BorshSerialize for StakeState { + fn serialize(&self, writer: &mut W) -> io::Result<()> { + match self { + StakeState::Uninitialized => writer.write_all(&0u32.to_le_bytes()), + StakeState::Initialized(meta) => { + writer.write_all(&1u32.to_le_bytes())?; + $borsh::BorshSerialize::serialize(&meta, writer) + } + StakeState::Stake(meta, stake) => { + writer.write_all(&2u32.to_le_bytes())?; + $borsh::BorshSerialize::serialize(&meta, writer)?; + $borsh::BorshSerialize::serialize(&stake, writer) + } + StakeState::RewardsPool => writer.write_all(&3u32.to_le_bytes()), + } + } + } + }; +} +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy)] +#[allow(clippy::large_enum_variant)] +#[deprecated( + since = "1.17.0", + note = "Please use `StakeStateV2` instead, and match the third `StakeFlags` field when matching `StakeStateV2::Stake` to resolve any breakage. For example, `if let StakeState::Stake(meta, stake)` becomes `if let StakeStateV2::Stake(meta, stake, _stake_flags)`." +)] +pub enum StakeState { + #[default] + Uninitialized, + Initialized(Meta), + Stake(Meta, Stake), + RewardsPool, +} +#[cfg(feature = "borsh")] +impl_borsh_stake_state!(borsh); +#[cfg(feature = "borsh")] +impl_borsh_stake_state!(borsh0_10); +impl StakeState { + /// The fixed number of bytes used to serialize each stake account + pub const fn size_of() -> usize { + 200 // see test_size_of + } + + pub fn stake(&self) -> Option { + match self { + Self::Stake(_meta, stake) => Some(*stake), + Self::Uninitialized | Self::Initialized(_) | Self::RewardsPool => None, + } + } + + pub fn delegation(&self) -> Option { + match self { + Self::Stake(_meta, stake) => Some(stake.delegation), + Self::Uninitialized | Self::Initialized(_) | Self::RewardsPool => None, + } + } + + pub fn authorized(&self) -> Option { + match self { + Self::Stake(meta, _stake) => Some(meta.authorized), + Self::Initialized(meta) => Some(meta.authorized), + Self::Uninitialized | Self::RewardsPool => None, + } + } + + pub fn lockup(&self) -> Option { + self.meta().map(|meta| meta.lockup) + } + + pub fn meta(&self) -> Option { + match self { + Self::Stake(meta, _stake) => Some(*meta), + Self::Initialized(meta) => Some(*meta), + Self::Uninitialized | Self::RewardsPool => None, + } + } +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy)] +#[allow(clippy::large_enum_variant)] +pub enum StakeStateV2 { + #[default] + Uninitialized, + Initialized(Meta), + Stake(Meta, Stake, StakeFlags), + RewardsPool, +} +#[cfg(feature = "borsh")] +macro_rules! impl_borsh_stake_state_v2 { + ($borsh:ident) => { + impl $borsh::BorshDeserialize for StakeStateV2 { + fn deserialize_reader(reader: &mut R) -> io::Result { + let enum_value: u32 = $borsh::BorshDeserialize::deserialize_reader(reader)?; + match enum_value { + 0 => Ok(StakeStateV2::Uninitialized), + 1 => { + let meta: Meta = $borsh::BorshDeserialize::deserialize_reader(reader)?; + Ok(StakeStateV2::Initialized(meta)) + } + 2 => { + let meta: Meta = $borsh::BorshDeserialize::deserialize_reader(reader)?; + let stake: Stake = $borsh::BorshDeserialize::deserialize_reader(reader)?; + let stake_flags: StakeFlags = + $borsh::BorshDeserialize::deserialize_reader(reader)?; + Ok(StakeStateV2::Stake(meta, stake, stake_flags)) + } + 3 => Ok(StakeStateV2::RewardsPool), + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid enum value", + )), + } + } + } + impl $borsh::BorshSerialize for StakeStateV2 { + fn serialize(&self, writer: &mut W) -> io::Result<()> { + match self { + StakeStateV2::Uninitialized => writer.write_all(&0u32.to_le_bytes()), + StakeStateV2::Initialized(meta) => { + writer.write_all(&1u32.to_le_bytes())?; + $borsh::BorshSerialize::serialize(&meta, writer) + } + StakeStateV2::Stake(meta, stake, stake_flags) => { + writer.write_all(&2u32.to_le_bytes())?; + $borsh::BorshSerialize::serialize(&meta, writer)?; + $borsh::BorshSerialize::serialize(&stake, writer)?; + $borsh::BorshSerialize::serialize(&stake_flags, writer) + } + StakeStateV2::RewardsPool => writer.write_all(&3u32.to_le_bytes()), + } + } + } + }; +} +#[cfg(feature = "borsh")] +impl_borsh_stake_state_v2!(borsh); +#[cfg(feature = "borsh")] +impl_borsh_stake_state_v2!(borsh0_10); + +impl StakeStateV2 { + /// The fixed number of bytes used to serialize each stake account + pub const fn size_of() -> usize { + 200 // see test_size_of + } + + pub fn stake(&self) -> Option { + match self { + Self::Stake(_meta, stake, _stake_flags) => Some(*stake), + Self::Uninitialized | Self::Initialized(_) | Self::RewardsPool => None, + } + } + + pub fn stake_ref(&self) -> Option<&Stake> { + match self { + Self::Stake(_meta, stake, _stake_flags) => Some(stake), + Self::Uninitialized | Self::Initialized(_) | Self::RewardsPool => None, + } + } + + pub fn delegation(&self) -> Option { + match self { + Self::Stake(_meta, stake, _stake_flags) => Some(stake.delegation), + Self::Uninitialized | Self::Initialized(_) | Self::RewardsPool => None, + } + } + + pub fn delegation_ref(&self) -> Option<&Delegation> { + match self { + StakeStateV2::Stake(_meta, stake, _stake_flags) => Some(&stake.delegation), + Self::Uninitialized | Self::Initialized(_) | Self::RewardsPool => None, + } + } + + pub fn authorized(&self) -> Option { + match self { + Self::Stake(meta, _stake, _stake_flags) => Some(meta.authorized), + Self::Initialized(meta) => Some(meta.authorized), + Self::Uninitialized | Self::RewardsPool => None, + } + } + + pub fn lockup(&self) -> Option { + self.meta().map(|meta| meta.lockup) + } + + pub fn meta(&self) -> Option { + match self { + Self::Stake(meta, _stake, _stake_flags) => Some(*meta), + Self::Initialized(meta) => Some(*meta), + Self::Uninitialized | Self::RewardsPool => None, + } + } +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] +pub enum StakeAuthorize { + Staker, + Withdrawer, +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr( + feature = "borsh", + derive(BorshSerialize, BorshDeserialize, BorshSchema), + borsh(crate = "borsh") +)] +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] +pub struct Lockup { + /// UnixTimestamp at which this stake will allow withdrawal, unless the + /// transaction is signed by the custodian + pub unix_timestamp: UnixTimestamp, + /// epoch height at which this stake will allow withdrawal, unless the + /// transaction is signed by the custodian + pub epoch: Epoch, + /// custodian signature on a transaction exempts the operation from + /// lockup constraints + pub custodian: Pubkey, +} +impl Lockup { + pub fn is_in_force(&self, clock: &Clock, custodian: Option<&Pubkey>) -> bool { + if custodian == Some(&self.custodian) { + return false; + } + self.unix_timestamp > clock.unix_timestamp || self.epoch > clock.epoch + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::de::BorshDeserialize for Lockup { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + unix_timestamp: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + epoch: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + custodian: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + }) + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::BorshSchema for Lockup { + fn declaration() -> borsh0_10::schema::Declaration { + "Lockup".to_string() + } + fn add_definitions_recursively( + definitions: &mut borsh0_10::maybestd::collections::HashMap< + borsh0_10::schema::Declaration, + borsh0_10::schema::Definition, + >, + ) { + let fields = borsh0_10::schema::Fields::NamedFields(<[_]>::into_vec( + borsh0_10::maybestd::boxed::Box::new([ + ( + "unix_timestamp".to_string(), + ::declaration(), + ), + ( + "epoch".to_string(), + ::declaration(), + ), + ( + "custodian".to_string(), + ::declaration(), + ), + ]), + )); + let definition = borsh0_10::schema::Definition::Struct { fields }; + Self::add_definition( + ::declaration(), + definition, + definitions, + ); + ::add_definitions_recursively(definitions); + ::add_definitions_recursively(definitions); + ::add_definitions_recursively(definitions); + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::ser::BorshSerialize for Lockup { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh0_10::maybestd::io::Error> { + borsh0_10::BorshSerialize::serialize(&self.unix_timestamp, writer)?; + borsh0_10::BorshSerialize::serialize(&self.epoch, writer)?; + borsh0_10::BorshSerialize::serialize(&self.custodian, writer)?; + Ok(()) + } +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr( + feature = "borsh", + derive(BorshSerialize, BorshDeserialize, BorshSchema), + borsh(crate = "borsh") +)] +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] +pub struct Authorized { + pub staker: Pubkey, + pub withdrawer: Pubkey, +} + +impl Authorized { + pub fn auto(authorized: &Pubkey) -> Self { + Self { + staker: *authorized, + withdrawer: *authorized, + } + } + pub fn check( + &self, + signers: &HashSet, + stake_authorize: StakeAuthorize, + ) -> Result<(), InstructionError> { + let authorized_signer = match stake_authorize { + StakeAuthorize::Staker => &self.staker, + StakeAuthorize::Withdrawer => &self.withdrawer, + }; + + if signers.contains(authorized_signer) { + Ok(()) + } else { + Err(InstructionError::MissingRequiredSignature) + } + } + + pub fn authorize( + &mut self, + signers: &HashSet, + new_authorized: &Pubkey, + stake_authorize: StakeAuthorize, + lockup_custodian_args: Option<(&Lockup, &Clock, Option<&Pubkey>)>, + ) -> Result<(), InstructionError> { + match stake_authorize { + StakeAuthorize::Staker => { + // Allow either the staker or the withdrawer to change the staker key + if !signers.contains(&self.staker) && !signers.contains(&self.withdrawer) { + return Err(InstructionError::MissingRequiredSignature); + } + self.staker = *new_authorized + } + StakeAuthorize::Withdrawer => { + if let Some((lockup, clock, custodian)) = lockup_custodian_args { + if lockup.is_in_force(clock, None) { + match custodian { + None => { + return Err(StakeError::CustodianMissing.into()); + } + Some(custodian) => { + if !signers.contains(custodian) { + return Err(StakeError::CustodianSignatureMissing.into()); + } + + if lockup.is_in_force(clock, Some(custodian)) { + return Err(StakeError::LockupInForce.into()); + } + } + } + } + } + self.check(signers, stake_authorize)?; + self.withdrawer = *new_authorized + } + } + Ok(()) + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::de::BorshDeserialize for Authorized { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + staker: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + withdrawer: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + }) + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::BorshSchema for Authorized { + fn declaration() -> borsh0_10::schema::Declaration { + "Authorized".to_string() + } + fn add_definitions_recursively( + definitions: &mut borsh0_10::maybestd::collections::HashMap< + borsh0_10::schema::Declaration, + borsh0_10::schema::Definition, + >, + ) { + let fields = borsh0_10::schema::Fields::NamedFields(<[_]>::into_vec( + borsh0_10::maybestd::boxed::Box::new([ + ( + "staker".to_string(), + ::declaration(), + ), + ( + "withdrawer".to_string(), + ::declaration(), + ), + ]), + )); + let definition = borsh0_10::schema::Definition::Struct { fields }; + Self::add_definition( + ::declaration(), + definition, + definitions, + ); + ::add_definitions_recursively(definitions); + ::add_definitions_recursively(definitions); + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::ser::BorshSerialize for Authorized { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh0_10::maybestd::io::Error> { + borsh0_10::BorshSerialize::serialize(&self.staker, writer)?; + borsh0_10::BorshSerialize::serialize(&self.withdrawer, writer)?; + Ok(()) + } +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr( + feature = "borsh", + derive(BorshSerialize, BorshDeserialize, BorshSchema), + borsh(crate = "borsh") +)] +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] +pub struct Meta { + pub rent_exempt_reserve: u64, + pub authorized: Authorized, + pub lockup: Lockup, +} + +impl Meta { + pub fn set_lockup( + &mut self, + lockup: &LockupArgs, + signers: &HashSet, + clock: &Clock, + ) -> Result<(), InstructionError> { + // post-stake_program_v4 behavior: + // * custodian can update the lockup while in force + // * withdraw authority can set a new lockup + if self.lockup.is_in_force(clock, None) { + if !signers.contains(&self.lockup.custodian) { + return Err(InstructionError::MissingRequiredSignature); + } + } else if !signers.contains(&self.authorized.withdrawer) { + return Err(InstructionError::MissingRequiredSignature); + } + if let Some(unix_timestamp) = lockup.unix_timestamp { + self.lockup.unix_timestamp = unix_timestamp; + } + if let Some(epoch) = lockup.epoch { + self.lockup.epoch = epoch; + } + if let Some(custodian) = lockup.custodian { + self.lockup.custodian = custodian; + } + Ok(()) + } + + pub fn auto(authorized: &Pubkey) -> Self { + Self { + authorized: Authorized::auto(authorized), + ..Meta::default() + } + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::de::BorshDeserialize for Meta { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + rent_exempt_reserve: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + authorized: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + lockup: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + }) + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::BorshSchema for Meta { + fn declaration() -> borsh0_10::schema::Declaration { + "Meta".to_string() + } + fn add_definitions_recursively( + definitions: &mut borsh0_10::maybestd::collections::HashMap< + borsh0_10::schema::Declaration, + borsh0_10::schema::Definition, + >, + ) { + let fields = borsh0_10::schema::Fields::NamedFields(<[_]>::into_vec( + borsh0_10::maybestd::boxed::Box::new([ + ( + "rent_exempt_reserve".to_string(), + ::declaration(), + ), + ( + "authorized".to_string(), + ::declaration(), + ), + ( + "lockup".to_string(), + ::declaration(), + ), + ]), + )); + let definition = borsh0_10::schema::Definition::Struct { fields }; + Self::add_definition( + ::declaration(), + definition, + definitions, + ); + ::add_definitions_recursively(definitions); + ::add_definitions_recursively(definitions); + ::add_definitions_recursively(definitions); + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::ser::BorshSerialize for Meta { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh0_10::maybestd::io::Error> { + borsh0_10::BorshSerialize::serialize(&self.rent_exempt_reserve, writer)?; + borsh0_10::BorshSerialize::serialize(&self.authorized, writer)?; + borsh0_10::BorshSerialize::serialize(&self.lockup, writer)?; + Ok(()) + } +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr( + feature = "borsh", + derive(BorshSerialize, BorshDeserialize, BorshSchema), + borsh(crate = "borsh") +)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] +pub struct Delegation { + /// to whom the stake is delegated + pub voter_pubkey: Pubkey, + /// activated stake amount, set at delegate() time + pub stake: u64, + /// epoch at which this stake was activated, std::Epoch::MAX if is a bootstrap stake + pub activation_epoch: Epoch, + /// epoch the stake was deactivated, std::Epoch::MAX if not deactivated + pub deactivation_epoch: Epoch, + /// how much stake we can activate per-epoch as a fraction of currently effective stake + #[deprecated( + since = "1.16.7", + note = "Please use `solana_sdk::stake::state::warmup_cooldown_rate()` instead" + )] + pub warmup_cooldown_rate: f64, +} + +impl Default for Delegation { + fn default() -> Self { + #[allow(deprecated)] + Self { + voter_pubkey: Pubkey::default(), + stake: 0, + activation_epoch: 0, + deactivation_epoch: u64::MAX, + warmup_cooldown_rate: DEFAULT_WARMUP_COOLDOWN_RATE, + } + } +} + +impl Delegation { + pub fn new(voter_pubkey: &Pubkey, stake: u64, activation_epoch: Epoch) -> Self { + Self { + voter_pubkey: *voter_pubkey, + stake, + activation_epoch, + ..Delegation::default() + } + } + pub fn is_bootstrap(&self) -> bool { + self.activation_epoch == u64::MAX + } + + pub fn stake( + &self, + epoch: Epoch, + history: &T, + new_rate_activation_epoch: Option, + ) -> u64 { + self.stake_activating_and_deactivating(epoch, history, new_rate_activation_epoch) + .effective + } + + #[allow(clippy::comparison_chain)] + pub fn stake_activating_and_deactivating( + &self, + target_epoch: Epoch, + history: &T, + new_rate_activation_epoch: Option, + ) -> StakeActivationStatus { + // first, calculate an effective and activating stake + let (effective_stake, activating_stake) = + self.stake_and_activating(target_epoch, history, new_rate_activation_epoch); + + // then de-activate some portion if necessary + if target_epoch < self.deactivation_epoch { + // not deactivated + if activating_stake == 0 { + StakeActivationStatus::with_effective(effective_stake) + } else { + StakeActivationStatus::with_effective_and_activating( + effective_stake, + activating_stake, + ) + } + } else if target_epoch == self.deactivation_epoch { + // can only deactivate what's activated + StakeActivationStatus::with_deactivating(effective_stake) + } else if let Some((history, mut prev_epoch, mut prev_cluster_stake)) = history + .get_entry(self.deactivation_epoch) + .map(|cluster_stake_at_deactivation_epoch| { + ( + history, + self.deactivation_epoch, + cluster_stake_at_deactivation_epoch, + ) + }) + { + // target_epoch > self.deactivation_epoch + + // loop from my deactivation epoch until the target epoch + // current effective stake is updated using its previous epoch's cluster stake + let mut current_epoch; + let mut current_effective_stake = effective_stake; + loop { + current_epoch = prev_epoch + 1; + // if there is no deactivating stake at prev epoch, we should have been + // fully undelegated at this moment + if prev_cluster_stake.deactivating == 0 { + break; + } + + // I'm trying to get to zero, how much of the deactivation in stake + // this account is entitled to take + let weight = + current_effective_stake as f64 / prev_cluster_stake.deactivating as f64; + let warmup_cooldown_rate = + warmup_cooldown_rate(current_epoch, new_rate_activation_epoch); + + // portion of newly not-effective cluster stake I'm entitled to at current epoch + let newly_not_effective_cluster_stake = + prev_cluster_stake.effective as f64 * warmup_cooldown_rate; + let newly_not_effective_stake = + ((weight * newly_not_effective_cluster_stake) as u64).max(1); + + current_effective_stake = + current_effective_stake.saturating_sub(newly_not_effective_stake); + if current_effective_stake == 0 { + break; + } + + if current_epoch >= target_epoch { + break; + } + if let Some(current_cluster_stake) = history.get_entry(current_epoch) { + prev_epoch = current_epoch; + prev_cluster_stake = current_cluster_stake; + } else { + break; + } + } + + // deactivating stake should equal to all of currently remaining effective stake + StakeActivationStatus::with_deactivating(current_effective_stake) + } else { + // no history or I've dropped out of history, so assume fully deactivated + StakeActivationStatus::default() + } + } + + // returned tuple is (effective, activating) stake + fn stake_and_activating( + &self, + target_epoch: Epoch, + history: &T, + new_rate_activation_epoch: Option, + ) -> (u64, u64) { + let delegated_stake = self.stake; + + if self.is_bootstrap() { + // fully effective immediately + (delegated_stake, 0) + } else if self.activation_epoch == self.deactivation_epoch { + // activated but instantly deactivated; no stake at all regardless of target_epoch + // this must be after the bootstrap check and before all-is-activating check + (0, 0) + } else if target_epoch == self.activation_epoch { + // all is activating + (0, delegated_stake) + } else if target_epoch < self.activation_epoch { + // not yet enabled + (0, 0) + } else if let Some((history, mut prev_epoch, mut prev_cluster_stake)) = history + .get_entry(self.activation_epoch) + .map(|cluster_stake_at_activation_epoch| { + ( + history, + self.activation_epoch, + cluster_stake_at_activation_epoch, + ) + }) + { + // target_epoch > self.activation_epoch + + // loop from my activation epoch until the target epoch summing up my entitlement + // current effective stake is updated using its previous epoch's cluster stake + let mut current_epoch; + let mut current_effective_stake = 0; + loop { + current_epoch = prev_epoch + 1; + // if there is no activating stake at prev epoch, we should have been + // fully effective at this moment + if prev_cluster_stake.activating == 0 { + break; + } + + // how much of the growth in stake this account is + // entitled to take + let remaining_activating_stake = delegated_stake - current_effective_stake; + let weight = + remaining_activating_stake as f64 / prev_cluster_stake.activating as f64; + let warmup_cooldown_rate = + warmup_cooldown_rate(current_epoch, new_rate_activation_epoch); + + // portion of newly effective cluster stake I'm entitled to at current epoch + let newly_effective_cluster_stake = + prev_cluster_stake.effective as f64 * warmup_cooldown_rate; + let newly_effective_stake = + ((weight * newly_effective_cluster_stake) as u64).max(1); + + current_effective_stake += newly_effective_stake; + if current_effective_stake >= delegated_stake { + current_effective_stake = delegated_stake; + break; + } + + if current_epoch >= target_epoch || current_epoch >= self.deactivation_epoch { + break; + } + if let Some(current_cluster_stake) = history.get_entry(current_epoch) { + prev_epoch = current_epoch; + prev_cluster_stake = current_cluster_stake; + } else { + break; + } + } + + ( + current_effective_stake, + delegated_stake - current_effective_stake, + ) + } else { + // no history or I've dropped out of history, so assume fully effective + (delegated_stake, 0) + } + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::de::BorshDeserialize for Delegation { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + voter_pubkey: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + stake: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + activation_epoch: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + deactivation_epoch: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + warmup_cooldown_rate: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + }) + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::BorshSchema for Delegation { + fn declaration() -> borsh0_10::schema::Declaration { + "Delegation".to_string() + } + fn add_definitions_recursively( + definitions: &mut borsh0_10::maybestd::collections::HashMap< + borsh0_10::schema::Declaration, + borsh0_10::schema::Definition, + >, + ) { + let fields = borsh0_10::schema::Fields::NamedFields(<[_]>::into_vec( + borsh0_10::maybestd::boxed::Box::new([ + ( + "voter_pubkey".to_string(), + ::declaration(), + ), + ( + "stake".to_string(), + ::declaration(), + ), + ( + "activation_epoch".to_string(), + ::declaration(), + ), + ( + "deactivation_epoch".to_string(), + ::declaration(), + ), + ( + "warmup_cooldown_rate".to_string(), + ::declaration(), + ), + ]), + )); + let definition = borsh0_10::schema::Definition::Struct { fields }; + Self::add_definition( + ::declaration(), + definition, + definitions, + ); + ::add_definitions_recursively(definitions); + ::add_definitions_recursively(definitions); + ::add_definitions_recursively(definitions); + ::add_definitions_recursively(definitions); + ::add_definitions_recursively(definitions); + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::ser::BorshSerialize for Delegation { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh0_10::maybestd::io::Error> { + borsh0_10::BorshSerialize::serialize(&self.voter_pubkey, writer)?; + borsh0_10::BorshSerialize::serialize(&self.stake, writer)?; + borsh0_10::BorshSerialize::serialize(&self.activation_epoch, writer)?; + borsh0_10::BorshSerialize::serialize(&self.deactivation_epoch, writer)?; + borsh0_10::BorshSerialize::serialize(&self.warmup_cooldown_rate, writer)?; + Ok(()) + } +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr( + feature = "borsh", + derive(BorshSerialize, BorshDeserialize, BorshSchema), + borsh(crate = "borsh") +)] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy)] +pub struct Stake { + pub delegation: Delegation, + /// credits observed is credits from vote account state when delegated or redeemed + pub credits_observed: u64, +} + +impl Stake { + pub fn stake( + &self, + epoch: Epoch, + history: &T, + new_rate_activation_epoch: Option, + ) -> u64 { + self.delegation + .stake(epoch, history, new_rate_activation_epoch) + } + + pub fn split( + &mut self, + remaining_stake_delta: u64, + split_stake_amount: u64, + ) -> Result { + if remaining_stake_delta > self.delegation.stake { + return Err(StakeError::InsufficientStake); + } + self.delegation.stake -= remaining_stake_delta; + let new = Self { + delegation: Delegation { + stake: split_stake_amount, + ..self.delegation + }, + ..*self + }; + Ok(new) + } + + pub fn deactivate(&mut self, epoch: Epoch) -> Result<(), StakeError> { + if self.delegation.deactivation_epoch != u64::MAX { + Err(StakeError::AlreadyDeactivated) + } else { + self.delegation.deactivation_epoch = epoch; + Ok(()) + } + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::de::BorshDeserialize for Stake { + fn deserialize_reader( + reader: &mut R, + ) -> ::core::result::Result { + Ok(Self { + delegation: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + credits_observed: borsh0_10::BorshDeserialize::deserialize_reader(reader)?, + }) + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::BorshSchema for Stake { + fn declaration() -> borsh0_10::schema::Declaration { + "Stake".to_string() + } + fn add_definitions_recursively( + definitions: &mut borsh0_10::maybestd::collections::HashMap< + borsh0_10::schema::Declaration, + borsh0_10::schema::Definition, + >, + ) { + let fields = borsh0_10::schema::Fields::NamedFields(<[_]>::into_vec( + borsh0_10::maybestd::boxed::Box::new([ + ( + "delegation".to_string(), + ::declaration(), + ), + ( + "credits_observed".to_string(), + ::declaration(), + ), + ]), + )); + let definition = borsh0_10::schema::Definition::Struct { fields }; + Self::add_definition( + ::declaration(), + definition, + definitions, + ); + ::add_definitions_recursively(definitions); + ::add_definitions_recursively(definitions); + } +} +#[cfg(feature = "borsh")] +impl borsh0_10::ser::BorshSerialize for Stake { + fn serialize( + &self, + writer: &mut W, + ) -> ::core::result::Result<(), borsh0_10::maybestd::io::Error> { + borsh0_10::BorshSerialize::serialize(&self.delegation, writer)?; + borsh0_10::BorshSerialize::serialize(&self.credits_observed, writer)?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + #[cfg(feature = "borsh")] + use crate::borsh1::try_from_slice_unchecked; + use {super::*, assert_matches::assert_matches, bincode::serialize}; + + #[cfg(feature = "borsh")] + fn check_borsh_deserialization(stake: StakeStateV2) { + let serialized = serialize(&stake).unwrap(); + let deserialized = StakeStateV2::try_from_slice(&serialized).unwrap(); + assert_eq!(stake, deserialized); + } + + #[cfg(feature = "borsh")] + fn check_borsh_serialization(stake: StakeStateV2) { + let bincode_serialized = serialize(&stake).unwrap(); + let borsh_serialized = borsh::to_vec(&stake).unwrap(); + assert_eq!(bincode_serialized, borsh_serialized); + } + + #[cfg(feature = "borsh")] + #[test] + fn test_size_of() { + assert_eq!(StakeStateV2::size_of(), std::mem::size_of::()); + } + + #[cfg(feature = "borsh")] + #[test] + fn bincode_vs_borsh_deserialization() { + check_borsh_deserialization(StakeStateV2::Uninitialized); + check_borsh_deserialization(StakeStateV2::RewardsPool); + check_borsh_deserialization(StakeStateV2::Initialized(Meta { + rent_exempt_reserve: u64::MAX, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + })); + check_borsh_deserialization(StakeStateV2::Stake( + Meta { + rent_exempt_reserve: 1, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + }, + Stake { + delegation: Delegation { + voter_pubkey: Pubkey::new_unique(), + stake: u64::MAX, + activation_epoch: Epoch::MAX, + deactivation_epoch: Epoch::MAX, + ..Delegation::default() + }, + credits_observed: 1, + }, + StakeFlags::empty(), + )); + } + + #[cfg(feature = "borsh")] + #[test] + fn bincode_vs_borsh_serialization() { + check_borsh_serialization(StakeStateV2::Uninitialized); + check_borsh_serialization(StakeStateV2::RewardsPool); + check_borsh_serialization(StakeStateV2::Initialized(Meta { + rent_exempt_reserve: u64::MAX, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + })); + #[allow(deprecated)] + check_borsh_serialization(StakeStateV2::Stake( + Meta { + rent_exempt_reserve: 1, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + }, + Stake { + delegation: Delegation { + voter_pubkey: Pubkey::new_unique(), + stake: u64::MAX, + activation_epoch: Epoch::MAX, + deactivation_epoch: Epoch::MAX, + ..Default::default() + }, + credits_observed: 1, + }, + StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED, + )); + } + + #[cfg(feature = "borsh")] + #[test] + fn borsh_deserialization_live_data() { + let data = [ + 1, 0, 0, 0, 128, 213, 34, 0, 0, 0, 0, 0, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35, + 119, 124, 168, 12, 120, 216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149, + 224, 109, 52, 100, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35, 119, 124, 168, 12, 120, + 216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149, 224, 109, 52, 100, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ]; + // As long as we get the 4-byte enum and the first field right, then + // we're sure the rest works out + let deserialized = try_from_slice_unchecked::(&data).unwrap(); + assert_matches!( + deserialized, + StakeStateV2::Initialized(Meta { + rent_exempt_reserve: 2282880, + .. + }) + ); + } + + #[test] + fn stake_flag_member_offset() { + const FLAG_OFFSET: usize = 196; + let check_flag = |flag, expected| { + let stake = StakeStateV2::Stake( + Meta { + rent_exempt_reserve: 1, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + }, + Stake { + delegation: Delegation { + voter_pubkey: Pubkey::new_unique(), + stake: u64::MAX, + activation_epoch: Epoch::MAX, + deactivation_epoch: Epoch::MAX, + warmup_cooldown_rate: f64::MAX, + }, + credits_observed: 1, + }, + flag, + ); + + let bincode_serialized = serialize(&stake).unwrap(); + let borsh_serialized = borsh::to_vec(&stake).unwrap(); + + assert_eq!(bincode_serialized[FLAG_OFFSET], expected); + assert_eq!(borsh_serialized[FLAG_OFFSET], expected); + }; + #[allow(deprecated)] + check_flag( + StakeFlags::MUST_FULLY_ACTIVATE_BEFORE_DEACTIVATION_IS_PERMITTED, + 1, + ); + check_flag(StakeFlags::empty(), 0); + } + + mod deprecated { + use super::*; + #[cfg(feature = "borsh")] + fn check_borsh_deserialization(stake: StakeState) { + let serialized = serialize(&stake).unwrap(); + let deserialized = StakeState::try_from_slice(&serialized).unwrap(); + assert_eq!(stake, deserialized); + } + + #[cfg(feature = "borsh")] + fn check_borsh_serialization(stake: StakeState) { + let bincode_serialized = serialize(&stake).unwrap(); + let borsh_serialized = borsh::to_vec(&stake).unwrap(); + assert_eq!(bincode_serialized, borsh_serialized); + } + + #[test] + fn test_size_of() { + assert_eq!(StakeState::size_of(), std::mem::size_of::()); + } + + #[cfg(feature = "borsh")] + #[test] + fn bincode_vs_borsh_deserialization() { + check_borsh_deserialization(StakeState::Uninitialized); + check_borsh_deserialization(StakeState::RewardsPool); + check_borsh_deserialization(StakeState::Initialized(Meta { + rent_exempt_reserve: u64::MAX, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + })); + check_borsh_deserialization(StakeState::Stake( + Meta { + rent_exempt_reserve: 1, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + }, + Stake { + delegation: Delegation { + voter_pubkey: Pubkey::new_unique(), + stake: u64::MAX, + activation_epoch: Epoch::MAX, + deactivation_epoch: Epoch::MAX, + warmup_cooldown_rate: f64::MAX, + }, + credits_observed: 1, + }, + )); + } + + #[cfg(feature = "borsh")] + #[test] + fn bincode_vs_borsh_serialization() { + check_borsh_serialization(StakeState::Uninitialized); + check_borsh_serialization(StakeState::RewardsPool); + check_borsh_serialization(StakeState::Initialized(Meta { + rent_exempt_reserve: u64::MAX, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + })); + check_borsh_serialization(StakeState::Stake( + Meta { + rent_exempt_reserve: 1, + authorized: Authorized { + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), + }, + lockup: Lockup::default(), + }, + Stake { + delegation: Delegation { + voter_pubkey: Pubkey::new_unique(), + stake: u64::MAX, + activation_epoch: Epoch::MAX, + deactivation_epoch: Epoch::MAX, + warmup_cooldown_rate: f64::MAX, + }, + credits_observed: 1, + }, + )); + } + + #[cfg(feature = "borsh")] + #[test] + fn borsh_deserialization_live_data() { + let data = [ + 1, 0, 0, 0, 128, 213, 34, 0, 0, 0, 0, 0, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35, + 119, 124, 168, 12, 120, 216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, + 149, 224, 109, 52, 100, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35, 119, 124, 168, + 12, 120, 216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149, 224, 109, 52, + 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + // As long as we get the 4-byte enum and the first field right, then + // we're sure the rest works out + let deserialized = try_from_slice_unchecked::(&data).unwrap(); + assert_matches!( + deserialized, + StakeState::Initialized(Meta { + rent_exempt_reserve: 2282880, + .. + }) + ); + } + } +} diff --git a/interface/src/tools.rs b/interface/src/tools.rs new file mode 100644 index 00000000..73f92017 --- /dev/null +++ b/interface/src/tools.rs @@ -0,0 +1,155 @@ +//! Utility functions +use { + crate::{program_error::ProgramError, stake::MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION}, + solana_clock::Epoch, +}; + +/// Helper function for programs to call [`GetMinimumDelegation`] and then fetch the return data +/// +/// This fn handles performing the CPI to call the [`GetMinimumDelegation`] function, and then +/// calls [`get_return_data()`] to fetch the return data. +/// +/// [`GetMinimumDelegation`]: super::instruction::StakeInstruction::GetMinimumDelegation +/// [`get_return_data()`]: crate::program::get_return_data +pub fn get_minimum_delegation() -> Result { + let instruction = super::instruction::get_minimum_delegation(); + crate::program::invoke_unchecked(&instruction, &[])?; + get_minimum_delegation_return_data() +} + +/// Helper function for programs to get the return data after calling [`GetMinimumDelegation`] +/// +/// This fn handles calling [`get_return_data()`], ensures the result is from the correct +/// program, and returns the correct type. +/// +/// [`GetMinimumDelegation`]: super::instruction::StakeInstruction::GetMinimumDelegation +/// [`get_return_data()`]: crate::program::get_return_data +fn get_minimum_delegation_return_data() -> Result { + crate::program::get_return_data() + .ok_or(ProgramError::InvalidInstructionData) + .and_then(|(program_id, return_data)| { + (program_id == super::program::id()) + .then_some(return_data) + .ok_or(ProgramError::IncorrectProgramId) + }) + .and_then(|return_data| { + return_data + .try_into() + .or(Err(ProgramError::InvalidInstructionData)) + }) + .map(u64::from_le_bytes) +} + +// Check if the provided `epoch_credits` demonstrate active voting over the previous +// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` +pub fn acceptable_reference_epoch_credits( + epoch_credits: &[(Epoch, u64, u64)], + current_epoch: Epoch, +) -> bool { + if let Some(epoch_index) = epoch_credits + .len() + .checked_sub(MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION) + { + let mut epoch = current_epoch; + for (vote_epoch, ..) in epoch_credits[epoch_index..].iter().rev() { + if *vote_epoch != epoch { + return false; + } + epoch = epoch.saturating_sub(1); + } + true + } else { + false + } +} + +// Check if the provided `epoch_credits` demonstrate delinquency over the previous +// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` +pub fn eligible_for_deactivate_delinquent( + epoch_credits: &[(Epoch, u64, u64)], + current_epoch: Epoch, +) -> bool { + match epoch_credits.last() { + None => true, + Some((epoch, ..)) => { + if let Some(minimum_epoch) = + current_epoch.checked_sub(MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch) + { + *epoch <= minimum_epoch + } else { + false + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_acceptable_reference_epoch_credits() { + let epoch_credits = []; + assert!(!acceptable_reference_epoch_credits(&epoch_credits, 0)); + + let epoch_credits = [(0, 42, 42), (1, 42, 42), (2, 42, 42), (3, 42, 42)]; + assert!(!acceptable_reference_epoch_credits(&epoch_credits, 3)); + + let epoch_credits = [ + (0, 42, 42), + (1, 42, 42), + (2, 42, 42), + (3, 42, 42), + (4, 42, 42), + ]; + assert!(!acceptable_reference_epoch_credits(&epoch_credits, 3)); + assert!(acceptable_reference_epoch_credits(&epoch_credits, 4)); + + let epoch_credits = [ + (1, 42, 42), + (2, 42, 42), + (3, 42, 42), + (4, 42, 42), + (5, 42, 42), + ]; + assert!(acceptable_reference_epoch_credits(&epoch_credits, 5)); + + let epoch_credits = [ + (0, 42, 42), + (2, 42, 42), + (3, 42, 42), + (4, 42, 42), + (5, 42, 42), + ]; + assert!(!acceptable_reference_epoch_credits(&epoch_credits, 5)); + } + + #[test] + fn test_eligible_for_deactivate_delinquent() { + let epoch_credits = []; + assert!(eligible_for_deactivate_delinquent(&epoch_credits, 42)); + + let epoch_credits = [(0, 42, 42)]; + assert!(!eligible_for_deactivate_delinquent(&epoch_credits, 0)); + + let epoch_credits = [(0, 42, 42)]; + assert!(!eligible_for_deactivate_delinquent( + &epoch_credits, + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch - 1 + )); + assert!(eligible_for_deactivate_delinquent( + &epoch_credits, + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch + )); + + let epoch_credits = [(100, 42, 42)]; + assert!(!eligible_for_deactivate_delinquent( + &epoch_credits, + 100 + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch - 1 + )); + assert!(eligible_for_deactivate_delinquent( + &epoch_credits, + 100 + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch + )); + } +}