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