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;
51#[deprecated(
52    since = "2.3.0",
53    note = "Use solana_sdk_ids::sysvar::stake_history instead"
54)]
55pub use solana_sdk_ids::sysvar::stake_history::{check_id, id, ID};
56#[deprecated(
57    since = "2.2.0",
58    note = "Use solana_stake_interface::stake_history instead"
59)]
60pub use solana_stake_interface::stake_history::{
61    StakeHistory, StakeHistoryEntry, StakeHistoryGetEntry, MAX_ENTRIES,
62};
63use {crate::get_sysvar, solana_clock::Epoch};
64
65#[cfg(feature = "bincode")]
66impl Sysvar for StakeHistory {
67    // override
68    fn size_of() -> usize {
69        // hard-coded so that we don't have to construct an empty
70        16392 // golden, update if MAX_ENTRIES changes
71    }
72}
73
74// we do not provide Default because this requires the real current epoch
75#[deprecated(
76    since = "2.3.0",
77    note = "Use solana_stake_interface::sysvar::stake_history::StakeHistorySysvar in solana-stake-interface v2 instead"
78)]
79#[derive(Debug, PartialEq, Eq, Clone)]
80pub struct StakeHistorySysvar(pub Epoch);
81
82// precompute so we can statically allocate buffer
83const EPOCH_AND_ENTRY_SERIALIZED_SIZE: u64 = 32;
84
85#[allow(deprecated)]
86impl StakeHistoryGetEntry for StakeHistorySysvar {
87    fn get_entry(&self, target_epoch: Epoch) -> Option<StakeHistoryEntry> {
88        let current_epoch = self.0;
89
90        // if current epoch is zero this returns None because there is no history yet
91        let newest_historical_epoch = current_epoch.checked_sub(1)?;
92        let oldest_historical_epoch = current_epoch.saturating_sub(MAX_ENTRIES as u64);
93
94        // target epoch is old enough to have fallen off history; presume fully active/deactive
95        if target_epoch < oldest_historical_epoch {
96            return None;
97        }
98
99        // epoch delta is how many epoch-entries we offset in the stake history vector, which may be zero
100        // None means target epoch is current or in the future; this is a user error
101        let epoch_delta = newest_historical_epoch.checked_sub(target_epoch)?;
102
103        // offset is the number of bytes to our desired entry, including eight for vector length
104        let offset = epoch_delta
105            .checked_mul(EPOCH_AND_ENTRY_SERIALIZED_SIZE)?
106            .checked_add(std::mem::size_of::<u64>() as u64)?;
107
108        let mut entry_buf = [0; EPOCH_AND_ENTRY_SERIALIZED_SIZE as usize];
109        let result = get_sysvar(
110            &mut entry_buf,
111            &id(),
112            offset,
113            EPOCH_AND_ENTRY_SERIALIZED_SIZE,
114        );
115
116        match result {
117            Ok(()) => {
118                // All safe because `entry_buf` is a 32-length array
119                let entry_epoch = u64::from_le_bytes(entry_buf[0..8].try_into().unwrap());
120                let effective = u64::from_le_bytes(entry_buf[8..16].try_into().unwrap());
121                let activating = u64::from_le_bytes(entry_buf[16..24].try_into().unwrap());
122                let deactivating = u64::from_le_bytes(entry_buf[24..32].try_into().unwrap());
123
124                // this would only fail if stake history skipped an epoch or the binary format of the sysvar changed
125                assert_eq!(entry_epoch, target_epoch);
126
127                Some(StakeHistoryEntry {
128                    effective,
129                    activating,
130                    deactivating,
131                })
132            }
133            _ => None,
134        }
135    }
136}
137
138#[cfg(test)]
139#[allow(deprecated)]
140mod tests {
141    use {super::*, crate::tests::mock_get_sysvar_syscall, serial_test::serial};
142
143    #[test]
144    fn test_size_of() {
145        let mut stake_history = StakeHistory::default();
146        for i in 0..MAX_ENTRIES as u64 {
147            stake_history.add(
148                i,
149                StakeHistoryEntry {
150                    activating: i,
151                    ..StakeHistoryEntry::default()
152                },
153            );
154        }
155
156        assert_eq!(
157            bincode::serialized_size(&stake_history).unwrap() as usize,
158            StakeHistory::size_of()
159        );
160
161        let stake_history_inner: Vec<(Epoch, StakeHistoryEntry)> =
162            bincode::deserialize(&bincode::serialize(&stake_history).unwrap()).unwrap();
163        let epoch_entry = stake_history_inner.into_iter().next().unwrap();
164
165        assert_eq!(
166            bincode::serialized_size(&epoch_entry).unwrap(),
167            EPOCH_AND_ENTRY_SERIALIZED_SIZE
168        );
169    }
170
171    #[serial]
172    #[test]
173    fn test_stake_history_get_entry() {
174        let unique_entry_for_epoch = |epoch: u64| StakeHistoryEntry {
175            activating: epoch.saturating_mul(2),
176            deactivating: epoch.saturating_mul(3),
177            effective: epoch.saturating_mul(5),
178        };
179
180        let current_epoch = MAX_ENTRIES.saturating_add(2) as u64;
181
182        // make a stake history object with at least one valid entry that has expired
183        let mut stake_history = StakeHistory::default();
184        for i in 0..current_epoch {
185            stake_history.add(i, unique_entry_for_epoch(i));
186        }
187        assert_eq!(stake_history.len(), MAX_ENTRIES);
188        assert_eq!(stake_history.iter().map(|entry| entry.0).min().unwrap(), 2);
189
190        // set up sol_get_sysvar
191        mock_get_sysvar_syscall(&bincode::serialize(&stake_history).unwrap());
192
193        // make a syscall interface object
194        let stake_history_sysvar = StakeHistorySysvar(current_epoch);
195
196        // now test the stake history interfaces
197
198        assert_eq!(stake_history.get(0), None);
199        assert_eq!(stake_history.get(1), None);
200        assert_eq!(stake_history.get(current_epoch), None);
201
202        assert_eq!(stake_history.get_entry(0), None);
203        assert_eq!(stake_history.get_entry(1), None);
204        assert_eq!(stake_history.get_entry(current_epoch), None);
205
206        assert_eq!(stake_history_sysvar.get_entry(0), None);
207        assert_eq!(stake_history_sysvar.get_entry(1), None);
208        assert_eq!(stake_history_sysvar.get_entry(current_epoch), None);
209
210        for i in 2..current_epoch {
211            let entry = Some(unique_entry_for_epoch(i));
212
213            assert_eq!(stake_history.get(i), entry.as_ref(),);
214
215            assert_eq!(stake_history.get_entry(i), entry,);
216
217            assert_eq!(stake_history_sysvar.get_entry(i), entry,);
218        }
219    }
220
221    #[serial]
222    #[test]
223    fn test_stake_history_get_entry_zero() {
224        let mut current_epoch = 0;
225
226        // first test that an empty history returns None
227        let stake_history = StakeHistory::default();
228        assert_eq!(stake_history.len(), 0);
229
230        mock_get_sysvar_syscall(&bincode::serialize(&stake_history).unwrap());
231        let stake_history_sysvar = StakeHistorySysvar(current_epoch);
232
233        assert_eq!(stake_history.get(0), None);
234        assert_eq!(stake_history.get_entry(0), None);
235        assert_eq!(stake_history_sysvar.get_entry(0), None);
236
237        // next test that we can get a zeroth entry in the first epoch
238        let entry_zero = StakeHistoryEntry {
239            effective: 100,
240            ..StakeHistoryEntry::default()
241        };
242        let entry = Some(entry_zero.clone());
243
244        let mut stake_history = StakeHistory::default();
245        stake_history.add(current_epoch, entry_zero);
246        assert_eq!(stake_history.len(), 1);
247        current_epoch = current_epoch.saturating_add(1);
248
249        mock_get_sysvar_syscall(&bincode::serialize(&stake_history).unwrap());
250        let stake_history_sysvar = StakeHistorySysvar(current_epoch);
251
252        assert_eq!(stake_history.get(0), entry.as_ref());
253        assert_eq!(stake_history.get_entry(0), entry);
254        assert_eq!(stake_history_sysvar.get_entry(0), entry);
255
256        // finally test that we can still get a zeroth entry in later epochs
257        stake_history.add(current_epoch, StakeHistoryEntry::default());
258        assert_eq!(stake_history.len(), 2);
259        current_epoch = current_epoch.saturating_add(1);
260
261        mock_get_sysvar_syscall(&bincode::serialize(&stake_history).unwrap());
262        let stake_history_sysvar = StakeHistorySysvar(current_epoch);
263
264        assert_eq!(stake_history.get(0), entry.as_ref());
265        assert_eq!(stake_history.get_entry(0), entry);
266        assert_eq!(stake_history_sysvar.get_entry(0), entry);
267    }
268}