solana_sysvar/
stake_history.rs

1//! History of stake activations and de-activations.
2//!
3//! The _stake history sysvar_ provides access to the [`StakeHistory`] type.
4//!
5//! The [`Sysvar::get`] method always returns
6//! [`ProgramError::UnsupportedSysvar`], and in practice the data size of this
7//! sysvar is too large to process on chain. One can still use the
8//! [`SysvarId::id`], [`SysvarId::check_id`] and [`Sysvar::size_of`] methods in
9//! an on-chain program, and it can be accessed off-chain through RPC.
10//!
11//! [`ProgramError::UnsupportedSysvar`]: https://docs.rs/solana-program-error/latest/solana_program_error/enum.ProgramError.html#variant.UnsupportedSysvar
12//! [`SysvarId::id`]: https://docs.rs/solana-sysvar-id/latest/solana_sysvar_id/trait.SysvarId.html
13//! [`SysvarId::check_id`]: https://docs.rs/solana-sysvar-id/latest/solana_sysvar_id/trait.SysvarId.html#tymethod.check_id
14//!
15//! # Examples
16//!
17//! Calling via the RPC client:
18//!
19//! ```
20//! # use solana_program::example_mocks::solana_sdk;
21//! # use solana_program::example_mocks::solana_rpc_client;
22//! # use solana_program::stake_history::StakeHistory;
23//! # use solana_sdk::account::Account;
24//! # use solana_rpc_client::rpc_client::RpcClient;
25//! # use solana_sdk_ids::sysvar::stake_history;
26//! # use anyhow::Result;
27//! #
28//! fn print_sysvar_stake_history(client: &RpcClient) -> Result<()> {
29//! #   client.set_get_account_response(stake_history::ID, Account {
30//! #       lamports: 114979200,
31//! #       data: vec![0, 0, 0, 0, 0, 0, 0, 0],
32//! #       owner: solana_sdk_ids::system_program::ID,
33//! #       executable: false,
34//! #       rent_epoch: 307,
35//! #   });
36//! #
37//!     let stake_history = client.get_account(&stake_history::ID)?;
38//!     let data: StakeHistory = bincode::deserialize(&stake_history.data)?;
39//!
40//!     Ok(())
41//! }
42//! #
43//! # let client = RpcClient::new(String::new());
44//! # print_sysvar_stake_history(&client)?;
45//! #
46//! # Ok::<(), anyhow::Error>(())
47//! ```
48
49#[cfg(feature = "bincode")]
50use crate::Sysvar;
51pub use solana_sdk_ids::sysvar::stake_history::{check_id, id, ID};
52#[deprecated(
53    since = "2.2.0",
54    note = "Use solana_stake_interface::stake_history instead"
55)]
56pub use solana_stake_interface::stake_history::{
57    StakeHistory, StakeHistoryEntry, StakeHistoryGetEntry, MAX_ENTRIES,
58};
59use {crate::get_sysvar, solana_clock::Epoch};
60
61#[cfg(feature = "bincode")]
62impl Sysvar for StakeHistory {
63    // override
64    fn size_of() -> usize {
65        // hard-coded so that we don't have to construct an empty
66        16392 // golden, update if MAX_ENTRIES changes
67    }
68}
69
70// we do not provide Default because this requires the real current epoch
71#[derive(Debug, PartialEq, Eq, Clone)]
72pub struct StakeHistorySysvar(pub Epoch);
73
74// precompute so we can statically allocate buffer
75const EPOCH_AND_ENTRY_SERIALIZED_SIZE: u64 = 32;
76
77impl StakeHistoryGetEntry for StakeHistorySysvar {
78    fn get_entry(&self, target_epoch: Epoch) -> Option<StakeHistoryEntry> {
79        let current_epoch = self.0;
80
81        // if current epoch is zero this returns None because there is no history yet
82        let newest_historical_epoch = current_epoch.checked_sub(1)?;
83        let oldest_historical_epoch = current_epoch.saturating_sub(MAX_ENTRIES as u64);
84
85        // target epoch is old enough to have fallen off history; presume fully active/deactive
86        if target_epoch < oldest_historical_epoch {
87            return None;
88        }
89
90        // epoch delta is how many epoch-entries we offset in the stake history vector, which may be zero
91        // None means target epoch is current or in the future; this is a user error
92        let epoch_delta = newest_historical_epoch.checked_sub(target_epoch)?;
93
94        // offset is the number of bytes to our desired entry, including eight for vector length
95        let offset = epoch_delta
96            .checked_mul(EPOCH_AND_ENTRY_SERIALIZED_SIZE)?
97            .checked_add(std::mem::size_of::<u64>() as u64)?;
98
99        let mut entry_buf = [0; EPOCH_AND_ENTRY_SERIALIZED_SIZE as usize];
100        let result = get_sysvar(
101            &mut entry_buf,
102            &id(),
103            offset,
104            EPOCH_AND_ENTRY_SERIALIZED_SIZE,
105        );
106
107        match result {
108            Ok(()) => {
109                // All safe because `entry_buf` is a 32-length array
110                let entry_epoch = u64::from_le_bytes(entry_buf[0..8].try_into().unwrap());
111                let effective = u64::from_le_bytes(entry_buf[8..16].try_into().unwrap());
112                let activating = u64::from_le_bytes(entry_buf[16..24].try_into().unwrap());
113                let deactivating = u64::from_le_bytes(entry_buf[24..32].try_into().unwrap());
114
115                // this would only fail if stake history skipped an epoch or the binary format of the sysvar changed
116                assert_eq!(entry_epoch, target_epoch);
117
118                Some(StakeHistoryEntry {
119                    effective,
120                    activating,
121                    deactivating,
122                })
123            }
124            _ => None,
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use {super::*, crate::tests::mock_get_sysvar_syscall, serial_test::serial};
132
133    #[test]
134    fn test_size_of() {
135        let mut stake_history = StakeHistory::default();
136        for i in 0..MAX_ENTRIES as u64 {
137            stake_history.add(
138                i,
139                StakeHistoryEntry {
140                    activating: i,
141                    ..StakeHistoryEntry::default()
142                },
143            );
144        }
145
146        assert_eq!(
147            bincode::serialized_size(&stake_history).unwrap() as usize,
148            StakeHistory::size_of()
149        );
150
151        let stake_history_inner: Vec<(Epoch, StakeHistoryEntry)> =
152            bincode::deserialize(&bincode::serialize(&stake_history).unwrap()).unwrap();
153        let epoch_entry = stake_history_inner.into_iter().next().unwrap();
154
155        assert_eq!(
156            bincode::serialized_size(&epoch_entry).unwrap(),
157            EPOCH_AND_ENTRY_SERIALIZED_SIZE
158        );
159    }
160
161    #[serial]
162    #[test]
163    fn test_stake_history_get_entry() {
164        let unique_entry_for_epoch = |epoch: u64| StakeHistoryEntry {
165            activating: epoch.saturating_mul(2),
166            deactivating: epoch.saturating_mul(3),
167            effective: epoch.saturating_mul(5),
168        };
169
170        let current_epoch = MAX_ENTRIES.saturating_add(2) as u64;
171
172        // make a stake history object with at least one valid entry that has expired
173        let mut stake_history = StakeHistory::default();
174        for i in 0..current_epoch {
175            stake_history.add(i, unique_entry_for_epoch(i));
176        }
177        assert_eq!(stake_history.len(), MAX_ENTRIES);
178        assert_eq!(stake_history.iter().map(|entry| entry.0).min().unwrap(), 2);
179
180        // set up sol_get_sysvar
181        mock_get_sysvar_syscall(&bincode::serialize(&stake_history).unwrap());
182
183        // make a syscall interface object
184        let stake_history_sysvar = StakeHistorySysvar(current_epoch);
185
186        // now test the stake history interfaces
187
188        assert_eq!(stake_history.get(0), None);
189        assert_eq!(stake_history.get(1), None);
190        assert_eq!(stake_history.get(current_epoch), None);
191
192        assert_eq!(stake_history.get_entry(0), None);
193        assert_eq!(stake_history.get_entry(1), None);
194        assert_eq!(stake_history.get_entry(current_epoch), None);
195
196        assert_eq!(stake_history_sysvar.get_entry(0), None);
197        assert_eq!(stake_history_sysvar.get_entry(1), None);
198        assert_eq!(stake_history_sysvar.get_entry(current_epoch), None);
199
200        for i in 2..current_epoch {
201            let entry = Some(unique_entry_for_epoch(i));
202
203            assert_eq!(stake_history.get(i), entry.as_ref(),);
204
205            assert_eq!(stake_history.get_entry(i), entry,);
206
207            assert_eq!(stake_history_sysvar.get_entry(i), entry,);
208        }
209    }
210
211    #[serial]
212    #[test]
213    fn test_stake_history_get_entry_zero() {
214        let mut current_epoch = 0;
215
216        // first test that an empty history returns None
217        let stake_history = StakeHistory::default();
218        assert_eq!(stake_history.len(), 0);
219
220        mock_get_sysvar_syscall(&bincode::serialize(&stake_history).unwrap());
221        let stake_history_sysvar = StakeHistorySysvar(current_epoch);
222
223        assert_eq!(stake_history.get(0), None);
224        assert_eq!(stake_history.get_entry(0), None);
225        assert_eq!(stake_history_sysvar.get_entry(0), None);
226
227        // next test that we can get a zeroth entry in the first epoch
228        let entry_zero = StakeHistoryEntry {
229            effective: 100,
230            ..StakeHistoryEntry::default()
231        };
232        let entry = Some(entry_zero.clone());
233
234        let mut stake_history = StakeHistory::default();
235        stake_history.add(current_epoch, entry_zero);
236        assert_eq!(stake_history.len(), 1);
237        current_epoch = current_epoch.saturating_add(1);
238
239        mock_get_sysvar_syscall(&bincode::serialize(&stake_history).unwrap());
240        let stake_history_sysvar = StakeHistorySysvar(current_epoch);
241
242        assert_eq!(stake_history.get(0), entry.as_ref());
243        assert_eq!(stake_history.get_entry(0), entry);
244        assert_eq!(stake_history_sysvar.get_entry(0), entry);
245
246        // finally test that we can still get a zeroth entry in later epochs
247        stake_history.add(current_epoch, StakeHistoryEntry::default());
248        assert_eq!(stake_history.len(), 2);
249        current_epoch = current_epoch.saturating_add(1);
250
251        mock_get_sysvar_syscall(&bincode::serialize(&stake_history).unwrap());
252        let stake_history_sysvar = StakeHistorySysvar(current_epoch);
253
254        assert_eq!(stake_history.get(0), entry.as_ref());
255        assert_eq!(stake_history.get_entry(0), entry);
256        assert_eq!(stake_history_sysvar.get_entry(0), entry);
257    }
258}