solomka_program/stake/
state.rs

1#![allow(clippy::integer_arithmetic)]
2use {
3    crate::{
4        clock::{Clock, Epoch, UnixTimestamp},
5        instruction::InstructionError,
6        pubkey::Pubkey,
7        stake::{
8            config::Config,
9            instruction::{LockupArgs, StakeError},
10        },
11        stake_history::{StakeHistory, StakeHistoryEntry},
12    },
13    borsh::{maybestd::io, BorshDeserialize, BorshSchema, BorshSerialize},
14    std::collections::HashSet,
15};
16
17pub type StakeActivationStatus = StakeHistoryEntry;
18
19#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)]
20#[allow(clippy::large_enum_variant)]
21pub enum StakeState {
22    Uninitialized,
23    Initialized(Meta),
24    Stake(Meta, Stake),
25    RewardsPool,
26}
27
28impl BorshDeserialize for StakeState {
29    fn deserialize(buf: &mut &[u8]) -> io::Result<Self> {
30        let enum_value: u32 = BorshDeserialize::deserialize(buf)?;
31        match enum_value {
32            0 => Ok(StakeState::Uninitialized),
33            1 => {
34                let meta: Meta = BorshDeserialize::deserialize(buf)?;
35                Ok(StakeState::Initialized(meta))
36            }
37            2 => {
38                let meta: Meta = BorshDeserialize::deserialize(buf)?;
39                let stake: Stake = BorshDeserialize::deserialize(buf)?;
40                Ok(StakeState::Stake(meta, stake))
41            }
42            3 => Ok(StakeState::RewardsPool),
43            _ => Err(io::Error::new(
44                io::ErrorKind::InvalidData,
45                "Invalid enum value",
46            )),
47        }
48    }
49}
50
51impl BorshSerialize for StakeState {
52    fn serialize<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
53        match self {
54            StakeState::Uninitialized => writer.write_all(&0u32.to_le_bytes()),
55            StakeState::Initialized(meta) => {
56                writer.write_all(&1u32.to_le_bytes())?;
57                meta.serialize(writer)
58            }
59            StakeState::Stake(meta, stake) => {
60                writer.write_all(&2u32.to_le_bytes())?;
61                meta.serialize(writer)?;
62                stake.serialize(writer)
63            }
64            StakeState::RewardsPool => writer.write_all(&3u32.to_le_bytes()),
65        }
66    }
67}
68
69impl Default for StakeState {
70    fn default() -> Self {
71        StakeState::Uninitialized
72    }
73}
74
75impl StakeState {
76    /// The fixed number of bytes used to serialize each stake account
77    pub const fn size_of() -> usize {
78        200 // see test_size_of
79    }
80
81    pub fn stake(&self) -> Option<Stake> {
82        match self {
83            StakeState::Stake(_meta, stake) => Some(*stake),
84            _ => None,
85        }
86    }
87
88    pub fn delegation(&self) -> Option<Delegation> {
89        match self {
90            StakeState::Stake(_meta, stake) => Some(stake.delegation),
91            _ => None,
92        }
93    }
94
95    pub fn authorized(&self) -> Option<Authorized> {
96        match self {
97            StakeState::Stake(meta, _stake) => Some(meta.authorized),
98            StakeState::Initialized(meta) => Some(meta.authorized),
99            _ => None,
100        }
101    }
102
103    pub fn lockup(&self) -> Option<Lockup> {
104        self.meta().map(|meta| meta.lockup)
105    }
106
107    pub fn meta(&self) -> Option<Meta> {
108        match self {
109            StakeState::Stake(meta, _stake) => Some(*meta),
110            StakeState::Initialized(meta) => Some(*meta),
111            _ => None,
112        }
113    }
114}
115
116#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, AbiExample)]
117pub enum StakeAuthorize {
118    Staker,
119    Withdrawer,
120}
121
122#[derive(
123    Default,
124    Debug,
125    Serialize,
126    Deserialize,
127    PartialEq,
128    Eq,
129    Clone,
130    Copy,
131    AbiExample,
132    BorshDeserialize,
133    BorshSchema,
134    BorshSerialize,
135)]
136pub struct Lockup {
137    /// UnixTimestamp at which this stake will allow withdrawal, unless the
138    ///   transaction is signed by the custodian
139    pub unix_timestamp: UnixTimestamp,
140    /// epoch height at which this stake will allow withdrawal, unless the
141    ///   transaction is signed by the custodian
142    pub epoch: Epoch,
143    /// custodian signature on a transaction exempts the operation from
144    ///  lockup constraints
145    pub custodian: Pubkey,
146}
147
148impl Lockup {
149    pub fn is_in_force(&self, clock: &Clock, custodian: Option<&Pubkey>) -> bool {
150        if custodian == Some(&self.custodian) {
151            return false;
152        }
153        self.unix_timestamp > clock.unix_timestamp || self.epoch > clock.epoch
154    }
155}
156
157#[derive(
158    Default,
159    Debug,
160    Serialize,
161    Deserialize,
162    PartialEq,
163    Eq,
164    Clone,
165    Copy,
166    AbiExample,
167    BorshDeserialize,
168    BorshSchema,
169    BorshSerialize,
170)]
171pub struct Authorized {
172    pub staker: Pubkey,
173    pub withdrawer: Pubkey,
174}
175
176impl Authorized {
177    pub fn auto(authorized: &Pubkey) -> Self {
178        Self {
179            staker: *authorized,
180            withdrawer: *authorized,
181        }
182    }
183    pub fn check(
184        &self,
185        signers: &HashSet<Pubkey>,
186        stake_authorize: StakeAuthorize,
187    ) -> Result<(), InstructionError> {
188        match stake_authorize {
189            StakeAuthorize::Staker if signers.contains(&self.staker) => Ok(()),
190            StakeAuthorize::Withdrawer if signers.contains(&self.withdrawer) => Ok(()),
191            _ => Err(InstructionError::MissingRequiredSignature),
192        }
193    }
194
195    pub fn authorize(
196        &mut self,
197        signers: &HashSet<Pubkey>,
198        new_authorized: &Pubkey,
199        stake_authorize: StakeAuthorize,
200        lockup_custodian_args: Option<(&Lockup, &Clock, Option<&Pubkey>)>,
201    ) -> Result<(), InstructionError> {
202        match stake_authorize {
203            StakeAuthorize::Staker => {
204                // Allow either the staker or the withdrawer to change the staker key
205                if !signers.contains(&self.staker) && !signers.contains(&self.withdrawer) {
206                    return Err(InstructionError::MissingRequiredSignature);
207                }
208                self.staker = *new_authorized
209            }
210            StakeAuthorize::Withdrawer => {
211                if let Some((lockup, clock, custodian)) = lockup_custodian_args {
212                    if lockup.is_in_force(clock, None) {
213                        match custodian {
214                            None => {
215                                return Err(StakeError::CustodianMissing.into());
216                            }
217                            Some(custodian) => {
218                                if !signers.contains(custodian) {
219                                    return Err(StakeError::CustodianSignatureMissing.into());
220                                }
221
222                                if lockup.is_in_force(clock, Some(custodian)) {
223                                    return Err(StakeError::LockupInForce.into());
224                                }
225                            }
226                        }
227                    }
228                }
229                self.check(signers, stake_authorize)?;
230                self.withdrawer = *new_authorized
231            }
232        }
233        Ok(())
234    }
235}
236
237#[derive(
238    Default,
239    Debug,
240    Serialize,
241    Deserialize,
242    PartialEq,
243    Eq,
244    Clone,
245    Copy,
246    AbiExample,
247    BorshDeserialize,
248    BorshSchema,
249    BorshSerialize,
250)]
251pub struct Meta {
252    pub rent_exempt_reserve: u64,
253    pub authorized: Authorized,
254    pub lockup: Lockup,
255}
256
257impl Meta {
258    pub fn set_lockup(
259        &mut self,
260        lockup: &LockupArgs,
261        signers: &HashSet<Pubkey>,
262        clock: &Clock,
263    ) -> Result<(), InstructionError> {
264        // post-stake_program_v4 behavior:
265        // * custodian can update the lockup while in force
266        // * withdraw authority can set a new lockup
267        if self.lockup.is_in_force(clock, None) {
268            if !signers.contains(&self.lockup.custodian) {
269                return Err(InstructionError::MissingRequiredSignature);
270            }
271        } else if !signers.contains(&self.authorized.withdrawer) {
272            return Err(InstructionError::MissingRequiredSignature);
273        }
274        if let Some(unix_timestamp) = lockup.unix_timestamp {
275            self.lockup.unix_timestamp = unix_timestamp;
276        }
277        if let Some(epoch) = lockup.epoch {
278            self.lockup.epoch = epoch;
279        }
280        if let Some(custodian) = lockup.custodian {
281            self.lockup.custodian = custodian;
282        }
283        Ok(())
284    }
285
286    pub fn auto(authorized: &Pubkey) -> Self {
287        Self {
288            authorized: Authorized::auto(authorized),
289            ..Meta::default()
290        }
291    }
292}
293
294#[derive(
295    Debug,
296    Serialize,
297    Deserialize,
298    PartialEq,
299    Clone,
300    Copy,
301    AbiExample,
302    BorshDeserialize,
303    BorshSchema,
304    BorshSerialize,
305)]
306pub struct Delegation {
307    /// to whom the stake is delegated
308    pub voter_pubkey: Pubkey,
309    /// activated stake amount, set at delegate() time
310    pub stake: u64,
311    /// epoch at which this stake was activated, std::Epoch::MAX if is a bootstrap stake
312    pub activation_epoch: Epoch,
313    /// epoch the stake was deactivated, std::Epoch::MAX if not deactivated
314    pub deactivation_epoch: Epoch,
315    /// how much stake we can activate per-epoch as a fraction of currently effective stake
316    pub warmup_cooldown_rate: f64,
317}
318
319impl Default for Delegation {
320    fn default() -> Self {
321        Self {
322            voter_pubkey: Pubkey::default(),
323            stake: 0,
324            activation_epoch: 0,
325            deactivation_epoch: std::u64::MAX,
326            warmup_cooldown_rate: Config::default().warmup_cooldown_rate,
327        }
328    }
329}
330
331impl Delegation {
332    pub fn new(
333        voter_pubkey: &Pubkey,
334        stake: u64,
335        activation_epoch: Epoch,
336        warmup_cooldown_rate: f64,
337    ) -> Self {
338        Self {
339            voter_pubkey: *voter_pubkey,
340            stake,
341            activation_epoch,
342            warmup_cooldown_rate,
343            ..Delegation::default()
344        }
345    }
346    pub fn is_bootstrap(&self) -> bool {
347        self.activation_epoch == std::u64::MAX
348    }
349
350    pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 {
351        self.stake_activating_and_deactivating(epoch, history)
352            .effective
353    }
354
355    #[allow(clippy::comparison_chain)]
356    pub fn stake_activating_and_deactivating(
357        &self,
358        target_epoch: Epoch,
359        history: Option<&StakeHistory>,
360    ) -> StakeActivationStatus {
361        // first, calculate an effective and activating stake
362        let (effective_stake, activating_stake) = self.stake_and_activating(target_epoch, history);
363
364        // then de-activate some portion if necessary
365        if target_epoch < self.deactivation_epoch {
366            // not deactivated
367            if activating_stake == 0 {
368                StakeActivationStatus::with_effective(effective_stake)
369            } else {
370                StakeActivationStatus::with_effective_and_activating(
371                    effective_stake,
372                    activating_stake,
373                )
374            }
375        } else if target_epoch == self.deactivation_epoch {
376            // can only deactivate what's activated
377            StakeActivationStatus::with_deactivating(effective_stake)
378        } else if let Some((history, mut prev_epoch, mut prev_cluster_stake)) =
379            history.and_then(|history| {
380                history
381                    .get(self.deactivation_epoch)
382                    .map(|cluster_stake_at_deactivation_epoch| {
383                        (
384                            history,
385                            self.deactivation_epoch,
386                            cluster_stake_at_deactivation_epoch,
387                        )
388                    })
389            })
390        {
391            // target_epoch > self.deactivation_epoch
392
393            // loop from my deactivation epoch until the target epoch
394            // current effective stake is updated using its previous epoch's cluster stake
395            let mut current_epoch;
396            let mut current_effective_stake = effective_stake;
397            loop {
398                current_epoch = prev_epoch + 1;
399                // if there is no deactivating stake at prev epoch, we should have been
400                // fully undelegated at this moment
401                if prev_cluster_stake.deactivating == 0 {
402                    break;
403                }
404
405                // I'm trying to get to zero, how much of the deactivation in stake
406                //   this account is entitled to take
407                let weight =
408                    current_effective_stake as f64 / prev_cluster_stake.deactivating as f64;
409
410                // portion of newly not-effective cluster stake I'm entitled to at current epoch
411                let newly_not_effective_cluster_stake =
412                    prev_cluster_stake.effective as f64 * self.warmup_cooldown_rate;
413                let newly_not_effective_stake =
414                    ((weight * newly_not_effective_cluster_stake) as u64).max(1);
415
416                current_effective_stake =
417                    current_effective_stake.saturating_sub(newly_not_effective_stake);
418                if current_effective_stake == 0 {
419                    break;
420                }
421
422                if current_epoch >= target_epoch {
423                    break;
424                }
425                if let Some(current_cluster_stake) = history.get(current_epoch) {
426                    prev_epoch = current_epoch;
427                    prev_cluster_stake = current_cluster_stake;
428                } else {
429                    break;
430                }
431            }
432
433            // deactivating stake should equal to all of currently remaining effective stake
434            StakeActivationStatus::with_deactivating(current_effective_stake)
435        } else {
436            // no history or I've dropped out of history, so assume fully deactivated
437            StakeActivationStatus::default()
438        }
439    }
440
441    // returned tuple is (effective, activating) stake
442    fn stake_and_activating(
443        &self,
444        target_epoch: Epoch,
445        history: Option<&StakeHistory>,
446    ) -> (u64, u64) {
447        let delegated_stake = self.stake;
448
449        if self.is_bootstrap() {
450            // fully effective immediately
451            (delegated_stake, 0)
452        } else if self.activation_epoch == self.deactivation_epoch {
453            // activated but instantly deactivated; no stake at all regardless of target_epoch
454            // this must be after the bootstrap check and before all-is-activating check
455            (0, 0)
456        } else if target_epoch == self.activation_epoch {
457            // all is activating
458            (0, delegated_stake)
459        } else if target_epoch < self.activation_epoch {
460            // not yet enabled
461            (0, 0)
462        } else if let Some((history, mut prev_epoch, mut prev_cluster_stake)) =
463            history.and_then(|history| {
464                history
465                    .get(self.activation_epoch)
466                    .map(|cluster_stake_at_activation_epoch| {
467                        (
468                            history,
469                            self.activation_epoch,
470                            cluster_stake_at_activation_epoch,
471                        )
472                    })
473            })
474        {
475            // target_epoch > self.activation_epoch
476
477            // loop from my activation epoch until the target epoch summing up my entitlement
478            // current effective stake is updated using its previous epoch's cluster stake
479            let mut current_epoch;
480            let mut current_effective_stake = 0;
481            loop {
482                current_epoch = prev_epoch + 1;
483                // if there is no activating stake at prev epoch, we should have been
484                // fully effective at this moment
485                if prev_cluster_stake.activating == 0 {
486                    break;
487                }
488
489                // how much of the growth in stake this account is
490                //  entitled to take
491                let remaining_activating_stake = delegated_stake - current_effective_stake;
492                let weight =
493                    remaining_activating_stake as f64 / prev_cluster_stake.activating as f64;
494
495                // portion of newly effective cluster stake I'm entitled to at current epoch
496                let newly_effective_cluster_stake =
497                    prev_cluster_stake.effective as f64 * self.warmup_cooldown_rate;
498                let newly_effective_stake =
499                    ((weight * newly_effective_cluster_stake) as u64).max(1);
500
501                current_effective_stake += newly_effective_stake;
502                if current_effective_stake >= delegated_stake {
503                    current_effective_stake = delegated_stake;
504                    break;
505                }
506
507                if current_epoch >= target_epoch || current_epoch >= self.deactivation_epoch {
508                    break;
509                }
510                if let Some(current_cluster_stake) = history.get(current_epoch) {
511                    prev_epoch = current_epoch;
512                    prev_cluster_stake = current_cluster_stake;
513                } else {
514                    break;
515                }
516            }
517
518            (
519                current_effective_stake,
520                delegated_stake - current_effective_stake,
521            )
522        } else {
523            // no history or I've dropped out of history, so assume fully effective
524            (delegated_stake, 0)
525        }
526    }
527}
528
529#[derive(
530    Debug,
531    Default,
532    Serialize,
533    Deserialize,
534    PartialEq,
535    Clone,
536    Copy,
537    AbiExample,
538    BorshDeserialize,
539    BorshSchema,
540    BorshSerialize,
541)]
542pub struct Stake {
543    pub delegation: Delegation,
544    /// credits observed is credits from vote account state when delegated or redeemed
545    pub credits_observed: u64,
546}
547
548impl Stake {
549    pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 {
550        self.delegation.stake(epoch, history)
551    }
552
553    pub fn split(
554        &mut self,
555        remaining_stake_delta: u64,
556        split_stake_amount: u64,
557    ) -> Result<Self, StakeError> {
558        if remaining_stake_delta > self.delegation.stake {
559            return Err(StakeError::InsufficientStake);
560        }
561        self.delegation.stake -= remaining_stake_delta;
562        let new = Self {
563            delegation: Delegation {
564                stake: split_stake_amount,
565                ..self.delegation
566            },
567            ..*self
568        };
569        Ok(new)
570    }
571
572    pub fn deactivate(&mut self, epoch: Epoch) -> Result<(), StakeError> {
573        if self.delegation.deactivation_epoch != std::u64::MAX {
574            Err(StakeError::AlreadyDeactivated)
575        } else {
576            self.delegation.deactivation_epoch = epoch;
577            Ok(())
578        }
579    }
580}
581
582#[cfg(test)]
583mod test {
584    use {
585        super::*, crate::borsh::try_from_slice_unchecked, assert_matches::assert_matches,
586        bincode::serialize,
587    };
588
589    fn check_borsh_deserialization(stake: StakeState) {
590        let serialized = serialize(&stake).unwrap();
591        let deserialized = StakeState::try_from_slice(&serialized).unwrap();
592        assert_eq!(stake, deserialized);
593    }
594
595    fn check_borsh_serialization(stake: StakeState) {
596        let bincode_serialized = serialize(&stake).unwrap();
597        let borsh_serialized = StakeState::try_to_vec(&stake).unwrap();
598        assert_eq!(bincode_serialized, borsh_serialized);
599    }
600
601    #[test]
602    fn test_size_of() {
603        assert_eq!(StakeState::size_of(), std::mem::size_of::<StakeState>());
604    }
605
606    #[test]
607    fn bincode_vs_borsh_deserialization() {
608        check_borsh_deserialization(StakeState::Uninitialized);
609        check_borsh_deserialization(StakeState::RewardsPool);
610        check_borsh_deserialization(StakeState::Initialized(Meta {
611            rent_exempt_reserve: u64::MAX,
612            authorized: Authorized {
613                staker: Pubkey::new_unique(),
614                withdrawer: Pubkey::new_unique(),
615            },
616            lockup: Lockup::default(),
617        }));
618        check_borsh_deserialization(StakeState::Stake(
619            Meta {
620                rent_exempt_reserve: 1,
621                authorized: Authorized {
622                    staker: Pubkey::new_unique(),
623                    withdrawer: Pubkey::new_unique(),
624                },
625                lockup: Lockup::default(),
626            },
627            Stake {
628                delegation: Delegation {
629                    voter_pubkey: Pubkey::new_unique(),
630                    stake: u64::MAX,
631                    activation_epoch: Epoch::MAX,
632                    deactivation_epoch: Epoch::MAX,
633                    warmup_cooldown_rate: f64::MAX,
634                },
635                credits_observed: 1,
636            },
637        ));
638    }
639
640    #[test]
641    fn bincode_vs_borsh_serialization() {
642        check_borsh_serialization(StakeState::Uninitialized);
643        check_borsh_serialization(StakeState::RewardsPool);
644        check_borsh_serialization(StakeState::Initialized(Meta {
645            rent_exempt_reserve: u64::MAX,
646            authorized: Authorized {
647                staker: Pubkey::new_unique(),
648                withdrawer: Pubkey::new_unique(),
649            },
650            lockup: Lockup::default(),
651        }));
652        check_borsh_serialization(StakeState::Stake(
653            Meta {
654                rent_exempt_reserve: 1,
655                authorized: Authorized {
656                    staker: Pubkey::new_unique(),
657                    withdrawer: Pubkey::new_unique(),
658                },
659                lockup: Lockup::default(),
660            },
661            Stake {
662                delegation: Delegation {
663                    voter_pubkey: Pubkey::new_unique(),
664                    stake: u64::MAX,
665                    activation_epoch: Epoch::MAX,
666                    deactivation_epoch: Epoch::MAX,
667                    warmup_cooldown_rate: f64::MAX,
668                },
669                credits_observed: 1,
670            },
671        ));
672    }
673
674    #[test]
675    fn borsh_deserialization_live_data() {
676        let data = [
677            1, 0, 0, 0, 128, 213, 34, 0, 0, 0, 0, 0, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35,
678            119, 124, 168, 12, 120, 216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149,
679            224, 109, 52, 100, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35, 119, 124, 168, 12, 120,
680            216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149, 224, 109, 52, 100, 0, 0,
681            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,
682            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,
683            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,
684            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,
685            0, 0, 0, 0, 0, 0,
686        ];
687        // As long as we get the 4-byte enum and the first field right, then
688        // we're sure the rest works out
689        let deserialized = try_from_slice_unchecked::<StakeState>(&data).unwrap();
690        assert_matches!(
691            deserialized,
692            StakeState::Initialized(Meta {
693                rent_exempt_reserve: 2282880,
694                ..
695            })
696        );
697    }
698}