atlas_sysvar/
slot_hashes.rs

1//! The most recent hashes of a slot's parent banks.
2//!
3//! The _slot hashes sysvar_ provides access to the [`SlotHashes`] type.
4//!
5//! The [`SysvarSerialize::from_account_info`] and [`Sysvar::get`] methods always return
6//! [`atlas_program_error::ProgramError::UnsupportedSysvar`] because this sysvar account is too large
7//! to process on-chain. Thus this sysvar cannot be accessed on chain, though
8//! one can still use the [`SysvarId::id`], [`SysvarId::check_id`] and
9//! [`SysvarSerialize::size_of`] methods in an on-chain program, and it can be accessed
10//! off-chain through RPC.
11//!
12//! [`SysvarId::id`]: https://docs.rs/atlas-sysvar-id/latest/atlas_sysvar_id/trait.SysvarId.html#tymethod.id
13//! [`SysvarId::check_id`]: https://docs.rs/atlas-sysvar-id/latest/atlas_sysvar_id/trait.SysvarId.html#tymethod.check_id
14//!
15//! # Examples
16//!
17//! Calling via the RPC client:
18//!
19//! ```
20//! # use atlas_example_mocks::atlas_account;
21//! # use atlas_example_mocks::atlas_rpc_client;
22//! # use atlas_account::Account;
23//! # use atlas_rpc_client::rpc_client::RpcClient;
24//! # use atlas_sdk_ids::sysvar::slot_hashes;
25//! # use atlas_slot_hashes::SlotHashes;
26//! # use anyhow::Result;
27//! #
28//! fn print_sysvar_slot_hashes(client: &RpcClient) -> Result<()> {
29//! #   client.set_get_account_response(slot_hashes::ID, Account {
30//! #       lamports: 1009200,
31//! #       data: vec![1, 0, 0, 0, 0, 0, 0, 0, 86, 190, 235, 7, 0, 0, 0, 0, 133, 242, 94, 158, 223, 253, 207, 184, 227, 194, 235, 27, 176, 98, 73, 3, 175, 201, 224, 111, 21, 65, 73, 27, 137, 73, 229, 19, 255, 192, 193, 126],
32//! #       owner: atlas_sdk_ids::system_program::ID,
33//! #       executable: false,
34//! # });
35//! #
36//!     let slot_hashes = client.get_account(&slot_hashes::ID)?;
37//!     let data: SlotHashes = bincode::deserialize(&slot_hashes.data)?;
38//!
39//!     Ok(())
40//! }
41//! #
42//! # let client = RpcClient::new(String::new());
43//! # print_sysvar_slot_hashes(&client)?;
44//! #
45//! # Ok::<(), anyhow::Error>(())
46//! ```
47#[cfg(feature = "bytemuck")]
48use bytemuck_derive::{Pod, Zeroable};
49use {crate::Sysvar, atlas_clock::Slot, atlas_hash::Hash};
50#[cfg(feature = "bincode")]
51use {crate::SysvarSerialize, atlas_account_info::AccountInfo};
52
53#[cfg(feature = "bytemuck")]
54const U64_SIZE: usize = std::mem::size_of::<u64>();
55
56#[cfg(any(feature = "bytemuck", feature = "bincode"))]
57const SYSVAR_LEN: usize = 20_488; // golden, update if MAX_ENTRIES changes
58
59pub use {
60    atlas_sdk_ids::sysvar::slot_hashes::{check_id, id, ID},
61    atlas_slot_hashes::SlotHashes,
62    atlas_sysvar_id::SysvarId,
63};
64
65impl Sysvar for SlotHashes {}
66#[cfg(feature = "bincode")]
67impl SysvarSerialize for SlotHashes {
68    // override
69    fn size_of() -> usize {
70        // hard-coded so that we don't have to construct an empty
71        SYSVAR_LEN
72    }
73    fn from_account_info(
74        _account_info: &AccountInfo,
75    ) -> Result<Self, atlas_program_error::ProgramError> {
76        // This sysvar is too large to bincode::deserialize in-program
77        Err(atlas_program_error::ProgramError::UnsupportedSysvar)
78    }
79}
80
81/// A bytemuck-compatible (plain old data) version of `SlotHash`.
82#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))]
83#[derive(Copy, Clone, Default)]
84#[repr(C)]
85pub struct PodSlotHash {
86    pub slot: Slot,
87    pub hash: Hash,
88}
89
90#[cfg(feature = "bytemuck")]
91/// API for querying of the `SlotHashes` sysvar by on-chain programs.
92///
93/// Hangs onto the allocated raw buffer from the account data, which can be
94/// queried or accessed directly as a slice of `PodSlotHash`.
95#[derive(Default)]
96pub struct PodSlotHashes {
97    data: Vec<u8>,
98    slot_hashes_start: usize,
99    slot_hashes_end: usize,
100}
101
102#[cfg(feature = "bytemuck")]
103impl PodSlotHashes {
104    /// Fetch all of the raw sysvar data using the `atlas_get_sysvar` syscall.
105    pub fn fetch() -> Result<Self, atlas_program_error::ProgramError> {
106        // Allocate an uninitialized buffer for the raw sysvar data.
107        let sysvar_len = SYSVAR_LEN;
108        let mut data = vec![0; sysvar_len];
109
110        // Ensure the created buffer is aligned to 8.
111        if data.as_ptr().align_offset(8) != 0 {
112            return Err(atlas_program_error::ProgramError::InvalidAccountData);
113        }
114
115        // Populate the buffer by fetching all sysvar data using the
116        // `atlas_get_sysvar` syscall.
117        crate::get_sysvar(
118            &mut data,
119            &SlotHashes::id(),
120            /* offset */ 0,
121            /* length */ sysvar_len as u64,
122        )?;
123
124        // Get the number of slot hashes present in the data by reading the
125        // `u64` length at the beginning of the data, then use that count to
126        // calculate the length of the slot hashes data.
127        //
128        // The rest of the buffer is uninitialized and should not be accessed.
129        let length = data
130            .get(..U64_SIZE)
131            .and_then(|bytes| bytes.try_into().ok())
132            .map(u64::from_le_bytes)
133            .and_then(|length| length.checked_mul(std::mem::size_of::<PodSlotHash>() as u64))
134            .ok_or(atlas_program_error::ProgramError::InvalidAccountData)?;
135
136        let slot_hashes_start = U64_SIZE;
137        let slot_hashes_end = slot_hashes_start.saturating_add(length as usize);
138
139        Ok(Self {
140            data,
141            slot_hashes_start,
142            slot_hashes_end,
143        })
144    }
145
146    /// Return the `SlotHashes` sysvar data as a slice of `PodSlotHash`.
147    /// Returns a slice of only the initialized sysvar data.
148    pub fn as_slice(&self) -> Result<&[PodSlotHash], atlas_program_error::ProgramError> {
149        self.data
150            .get(self.slot_hashes_start..self.slot_hashes_end)
151            .and_then(|data| bytemuck::try_cast_slice(data).ok())
152            .ok_or(atlas_program_error::ProgramError::InvalidAccountData)
153    }
154
155    /// Given a slot, get its corresponding hash in the `SlotHashes` sysvar
156    /// data. Returns `None` if the slot is not found.
157    pub fn get(&self, slot: &Slot) -> Result<Option<Hash>, atlas_program_error::ProgramError> {
158        self.as_slice().map(|pod_hashes| {
159            pod_hashes
160                .binary_search_by(|PodSlotHash { slot: this, .. }| slot.cmp(this))
161                .map(|idx| pod_hashes[idx].hash)
162                .ok()
163        })
164    }
165
166    /// Given a slot, get its position in the `SlotHashes` sysvar data. Returns
167    /// `None` if the slot is not found.
168    pub fn position(
169        &self,
170        slot: &Slot,
171    ) -> Result<Option<usize>, atlas_program_error::ProgramError> {
172        self.as_slice().map(|pod_hashes| {
173            pod_hashes
174                .binary_search_by(|PodSlotHash { slot: this, .. }| slot.cmp(this))
175                .ok()
176        })
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use {
183        super::*, crate::tests::mock_get_sysvar_syscall, serial_test::serial, atlas_hash::Hash,
184        atlas_sha256_hasher::hash, atlas_slot_hashes::MAX_ENTRIES, test_case::test_case,
185    };
186
187    #[test]
188    fn test_size_of() {
189        assert_eq!(
190            SlotHashes::size_of(),
191            bincode::serialized_size(
192                &(0..MAX_ENTRIES)
193                    .map(|slot| (slot as Slot, Hash::default()))
194                    .collect::<SlotHashes>()
195            )
196            .unwrap() as usize
197        );
198    }
199
200    fn mock_slot_hashes(slot_hashes: &SlotHashes) {
201        // The data is always `SlotHashes::size_of()`.
202        let mut data = vec![0; SlotHashes::size_of()];
203        bincode::serialize_into(&mut data[..], slot_hashes).unwrap();
204        mock_get_sysvar_syscall(&data);
205    }
206
207    #[test_case(0)]
208    #[test_case(1)]
209    #[test_case(2)]
210    #[test_case(5)]
211    #[test_case(10)]
212    #[test_case(64)]
213    #[test_case(128)]
214    #[test_case(192)]
215    #[test_case(256)]
216    #[test_case(384)]
217    #[test_case(MAX_ENTRIES)]
218    #[serial]
219    fn test_pod_slot_hashes(num_entries: usize) {
220        let mut slot_hashes = vec![];
221        for i in 0..num_entries {
222            slot_hashes.push((
223                i as u64,
224                hash(&[(i >> 24) as u8, (i >> 16) as u8, (i >> 8) as u8, i as u8]),
225            ));
226        }
227
228        let check_slot_hashes = SlotHashes::new(&slot_hashes);
229        mock_slot_hashes(&check_slot_hashes);
230
231        let pod_slot_hashes = PodSlotHashes::fetch().unwrap();
232
233        // Assert the slice of `PodSlotHash` has the same length as
234        // `SlotHashes`.
235        let pod_slot_hashes_slice = pod_slot_hashes.as_slice().unwrap();
236        assert_eq!(pod_slot_hashes_slice.len(), slot_hashes.len());
237
238        // Assert `PodSlotHashes` and `SlotHashes` contain the same slot hashes
239        // in the same order.
240        for slot in slot_hashes.iter().map(|(slot, _hash)| slot) {
241            // `get`:
242            assert_eq!(
243                pod_slot_hashes.get(slot).unwrap().as_ref(),
244                check_slot_hashes.get(slot),
245            );
246            // `position`:
247            assert_eq!(
248                pod_slot_hashes.position(slot).unwrap(),
249                check_slot_hashes.position(slot),
250            );
251        }
252
253        // Check a few `None` values.
254        let not_a_slot = num_entries.saturating_add(1) as u64;
255        assert_eq!(
256            pod_slot_hashes.get(&not_a_slot).unwrap().as_ref(),
257            check_slot_hashes.get(&not_a_slot),
258        );
259        assert_eq!(pod_slot_hashes.get(&not_a_slot).unwrap(), None);
260        assert_eq!(
261            pod_slot_hashes.position(&not_a_slot).unwrap(),
262            check_slot_hashes.position(&not_a_slot),
263        );
264        assert_eq!(pod_slot_hashes.position(&not_a_slot).unwrap(), None);
265
266        let not_a_slot = num_entries.saturating_add(2) as u64;
267        assert_eq!(
268            pod_slot_hashes.get(&not_a_slot).unwrap().as_ref(),
269            check_slot_hashes.get(&not_a_slot),
270        );
271        assert_eq!(pod_slot_hashes.get(&not_a_slot).unwrap(), None);
272        assert_eq!(
273            pod_slot_hashes.position(&not_a_slot).unwrap(),
274            check_slot_hashes.position(&not_a_slot),
275        );
276        assert_eq!(pod_slot_hashes.position(&not_a_slot).unwrap(), None);
277    }
278}