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