Skip to main content

solana_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 [`crate::Sysvar::get`] method always returns
6//! [`solana_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 [`SIZE`]
9//! in an on-chain program, and it can be accessed off-chain through RPC.
10//!
11//! [`SysvarId::id`]: https://docs.rs/solana-sysvar-id/latest/solana_sysvar_id/trait.SysvarId.html#tymethod.id
12//! [`SysvarId::check_id`]: https://docs.rs/solana-sysvar-id/latest/solana_sysvar_id/trait.SysvarId.html#tymethod.check_id
13//!
14//! # Examples
15//!
16//! Calling via the RPC client:
17//!
18//! ```
19//! # use solana_example_mocks::solana_account;
20//! # use solana_example_mocks::solana_rpc_client;
21//! # use solana_account::Account;
22//! # use solana_rpc_client::rpc_client::RpcClient;
23//! # use solana_sdk_ids::sysvar::slot_hashes;
24//! # use solana_slot_hashes::SlotHashes;
25//! # use anyhow::Result;
26//! #
27//! fn print_sysvar_slot_hashes(client: &RpcClient) -> Result<()> {
28//! #   client.set_get_account_response(slot_hashes::ID, Account {
29//! #       lamports: 1009200,
30//! #       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],
31//! #       owner: solana_sdk_ids::system_program::ID,
32//! #       executable: false,
33//! # });
34//! #
35//!     let slot_hashes = client.get_account(&slot_hashes::ID)?;
36//!     let data: SlotHashes = wincode::deserialize(&slot_hashes.data)?;
37//!
38//!     Ok(())
39//! }
40//! #
41//! # let client = RpcClient::new(String::new());
42//! # print_sysvar_slot_hashes(&client)?;
43//! #
44//! # Ok::<(), anyhow::Error>(())
45//! ```
46#[cfg(feature = "bytemuck")]
47use bytemuck_derive::{Pod, Zeroable};
48use {solana_clock::Slot, solana_hash::Hash};
49
50#[cfg(feature = "bytemuck")]
51const U64_SIZE: usize = std::mem::size_of::<u64>();
52
53pub use {
54    solana_sdk_ids::sysvar::slot_hashes::{check_id, id, ID},
55    solana_slot_hashes::{SlotHashes, SIZE},
56    solana_sysvar_id::SysvarId,
57};
58
59/// A bytemuck-compatible (plain old data) version of `SlotHash`.
60#[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable))]
61#[derive(Copy, Clone, Default)]
62#[repr(C)]
63pub struct PodSlotHash {
64    pub slot: Slot,
65    pub hash: Hash,
66}
67
68#[cfg(feature = "bytemuck")]
69/// API for querying of the `SlotHashes` sysvar by on-chain programs.
70///
71/// Hangs onto the allocated raw buffer from the account data, which can be
72/// queried or accessed directly as a slice of `PodSlotHash`.
73#[derive(Default)]
74pub struct PodSlotHashes {
75    data: Vec<u8>,
76    slot_hashes_start: usize,
77    slot_hashes_end: usize,
78}
79
80#[cfg(feature = "bytemuck")]
81impl PodSlotHashes {
82    /// Fetch all of the raw sysvar data using the `sol_get_sysvar` syscall.
83    pub fn fetch() -> Result<Self, solana_program_error::ProgramError> {
84        // Allocate an uninitialized buffer for the raw sysvar data.
85        let sysvar_len = SIZE;
86        let mut data = vec![0; sysvar_len];
87
88        // Ensure the created buffer is aligned to 8.
89        if data.as_ptr().align_offset(8) != 0 {
90            return Err(solana_program_error::ProgramError::InvalidAccountData);
91        }
92
93        // Populate the buffer by fetching all sysvar data using the
94        // `sol_get_sysvar` syscall.
95        crate::get_sysvar(
96            &mut data,
97            &SlotHashes::id(),
98            /* offset */ 0,
99            /* length */ sysvar_len as u64,
100        )?;
101
102        Self::from_bytes(data)
103    }
104
105    fn from_bytes(data: Vec<u8>) -> Result<Self, solana_program_error::ProgramError> {
106        // Get the number of slot hashes present in the data by reading the
107        // `u64` length at the beginning of the data, then use that count to
108        // calculate the length of the slot hashes data.
109        //
110        // The rest of the buffer is uninitialized and should not be accessed.
111        let length = data
112            .get(..U64_SIZE)
113            .and_then(|bytes| bytes.try_into().ok())
114            .map(u64::from_le_bytes)
115            .and_then(|length| length.checked_mul(std::mem::size_of::<PodSlotHash>() as u64))
116            .ok_or(solana_program_error::ProgramError::InvalidAccountData)?;
117
118        let slot_hashes_start = U64_SIZE;
119        let slot_hashes_end = slot_hashes_start.saturating_add(length as usize);
120
121        Ok(Self {
122            data,
123            slot_hashes_start,
124            slot_hashes_end,
125        })
126    }
127
128    /// Return the `SlotHashes` sysvar data as a slice of `PodSlotHash`.
129    /// Returns a slice of only the initialized sysvar data.
130    pub fn as_slice(&self) -> Result<&[PodSlotHash], solana_program_error::ProgramError> {
131        self.data
132            .get(self.slot_hashes_start..self.slot_hashes_end)
133            .and_then(|data| bytemuck::try_cast_slice(data).ok())
134            .ok_or(solana_program_error::ProgramError::InvalidAccountData)
135    }
136
137    /// Given a slot, get its corresponding hash in the `SlotHashes` sysvar
138    /// data. Returns `None` if the slot is not found.
139    pub fn get(&self, slot: &Slot) -> Result<Option<Hash>, solana_program_error::ProgramError> {
140        self.as_slice().map(|pod_hashes| {
141            pod_hashes
142                .binary_search_by(|PodSlotHash { slot: this, .. }| slot.cmp(this))
143                .map(|idx| pod_hashes[idx].hash)
144                .ok()
145        })
146    }
147
148    /// Given a slot, get its position in the `SlotHashes` sysvar data. Returns
149    /// `None` if the slot is not found.
150    pub fn position(
151        &self,
152        slot: &Slot,
153    ) -> Result<Option<usize>, solana_program_error::ProgramError> {
154        self.as_slice().map(|pod_hashes| {
155            pod_hashes
156                .binary_search_by(|PodSlotHash { slot: this, .. }| slot.cmp(this))
157                .ok()
158        })
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use {
165        super::*,
166        solana_sha256_hasher::hash,
167        solana_slot_hashes::{SlotHash, MAX_ENTRIES},
168        test_case::test_case,
169    };
170
171    #[test_case(0)]
172    #[test_case(1)]
173    #[test_case(2)]
174    #[test_case(5)]
175    #[test_case(10)]
176    #[test_case(64)]
177    #[test_case(128)]
178    #[test_case(192)]
179    #[test_case(256)]
180    #[test_case(384)]
181    #[test_case(MAX_ENTRIES)]
182    fn test_pod_slot_hashes(num_entries: usize) {
183        let mut slot_hashes = vec![];
184        for i in 0..num_entries {
185            slot_hashes.push(SlotHash::new(
186                i as u64,
187                hash(&[(i >> 24) as u8, (i >> 16) as u8, (i >> 8) as u8, i as u8]),
188            ));
189        }
190
191        let check_slot_hashes = SlotHashes::new(&slot_hashes);
192        let pod_slot_hashes =
193            PodSlotHashes::from_bytes(wincode::serialize(&check_slot_hashes).unwrap()).unwrap();
194
195        // Assert the slice of `PodSlotHash` has the same length as
196        // `SlotHashes`.
197        let pod_slot_hashes_slice = pod_slot_hashes.as_slice().unwrap();
198        assert_eq!(pod_slot_hashes_slice.len(), slot_hashes.len());
199
200        // Assert `PodSlotHashes` and `SlotHashes` contain the same slot hashes
201        // in the same order.
202        for slot in slot_hashes.iter().map(|entry| &entry.slot) {
203            // `get`:
204            assert_eq!(
205                pod_slot_hashes.get(slot).unwrap().as_ref(),
206                check_slot_hashes.get(slot),
207            );
208            // `position`:
209            assert_eq!(
210                pod_slot_hashes.position(slot).unwrap(),
211                check_slot_hashes.position(slot),
212            );
213        }
214
215        // Check a few `None` values.
216        let not_a_slot = num_entries.saturating_add(1) as u64;
217        assert_eq!(
218            pod_slot_hashes.get(&not_a_slot).unwrap().as_ref(),
219            check_slot_hashes.get(&not_a_slot),
220        );
221        assert_eq!(pod_slot_hashes.get(&not_a_slot).unwrap(), None);
222        assert_eq!(
223            pod_slot_hashes.position(&not_a_slot).unwrap(),
224            check_slot_hashes.position(&not_a_slot),
225        );
226        assert_eq!(pod_slot_hashes.position(&not_a_slot).unwrap(), None);
227
228        let not_a_slot = num_entries.saturating_add(2) as u64;
229        assert_eq!(
230            pod_slot_hashes.get(&not_a_slot).unwrap().as_ref(),
231            check_slot_hashes.get(&not_a_slot),
232        );
233        assert_eq!(pod_slot_hashes.get(&not_a_slot).unwrap(), None);
234        assert_eq!(
235            pod_slot_hashes.position(&not_a_slot).unwrap(),
236            check_slot_hashes.position(&not_a_slot),
237        );
238        assert_eq!(pod_slot_hashes.position(&not_a_slot).unwrap(), None);
239    }
240}