|
| 1 | +//! Efficient, zero-copy access to `SlotHashes` sysvar data. |
| 2 | +
|
| 3 | +pub mod raw; |
| 4 | +#[doc(inline)] |
| 5 | +pub use raw::{fetch_into, fetch_into_unchecked, validate_fetch_offset}; |
| 6 | + |
| 7 | +#[cfg(test)] |
| 8 | +mod test; |
| 9 | +#[cfg(test)] |
| 10 | +mod test_edge; |
| 11 | +#[cfg(test)] |
| 12 | +mod test_raw; |
| 13 | +#[cfg(test)] |
| 14 | +mod test_utils; |
| 15 | + |
| 16 | +use crate::{ |
| 17 | + account_info::{AccountInfo, Ref}, |
| 18 | + program_error::ProgramError, |
| 19 | + pubkey::Pubkey, |
| 20 | + sysvars::clock::Slot, |
| 21 | +}; |
| 22 | +use core::{mem, ops::Deref, slice::from_raw_parts}; |
| 23 | +#[cfg(feature = "std")] |
| 24 | +use std::boxed::Box; |
| 25 | + |
| 26 | +/// `SysvarS1otHashes111111111111111111111111111` |
| 27 | +pub const SLOTHASHES_ID: Pubkey = [ |
| 28 | + 6, 167, 213, 23, 25, 47, 10, 175, 198, 242, 101, 227, 251, 119, 204, 122, 218, 130, 197, 41, |
| 29 | + 208, 190, 59, 19, 110, 45, 0, 85, 32, 0, 0, 0, |
| 30 | +]; |
| 31 | +/// Number of bytes in a hash. |
| 32 | +pub const HASH_BYTES: usize = 32; |
| 33 | +/// Sysvar data is: |
| 34 | +/// `len` (8 bytes): little-endian entry count (`≤ 512`) |
| 35 | +/// `entries`(`len × 40 bytes`): consecutive `(u64 slot, [u8;32] hash)` pairs |
| 36 | +/// Size of the entry count field at the beginning of sysvar data. |
| 37 | +pub const NUM_ENTRIES_SIZE: usize = mem::size_of::<u64>(); |
| 38 | +/// Size of a slot number in bytes. |
| 39 | +pub const SLOT_SIZE: usize = mem::size_of::<Slot>(); |
| 40 | +/// Size of a single slot hash entry. |
| 41 | +pub const ENTRY_SIZE: usize = SLOT_SIZE + HASH_BYTES; |
| 42 | +/// Maximum number of slot hash entries that can be stored in the sysvar. |
| 43 | +pub const MAX_ENTRIES: usize = 512; |
| 44 | +/// Max size of the sysvar data in bytes. 20488. Golden on mainnet (never smaller) |
| 45 | +pub const MAX_SIZE: usize = NUM_ENTRIES_SIZE + MAX_ENTRIES * ENTRY_SIZE; |
| 46 | +/// A single hash. |
| 47 | +pub type Hash = [u8; HASH_BYTES]; |
| 48 | + |
| 49 | +/// A single entry in the `SlotHashes` sysvar. |
| 50 | +#[derive(Debug, PartialEq, Eq, Clone, Copy)] |
| 51 | +#[repr(C)] |
| 52 | +pub struct SlotHashEntry { |
| 53 | + /// The slot number stored as little-endian bytes. |
| 54 | + slot_le: [u8; 8], |
| 55 | + /// The hash corresponding to the slot. |
| 56 | + pub hash: Hash, |
| 57 | +} |
| 58 | + |
| 59 | +// Fail compilation if `SlotHashEntry` is not byte-aligned. |
| 60 | +const _: [(); 1] = [(); mem::align_of::<SlotHashEntry>()]; |
| 61 | + |
| 62 | +/// `SlotHashes` provides read-only, zero-copy access to `SlotHashes` sysvar bytes. |
| 63 | +pub struct SlotHashes<T: Deref<Target = [u8]>> { |
| 64 | + data: T, |
| 65 | +} |
| 66 | + |
| 67 | +/// Log a `Hash` from a program. |
| 68 | +pub fn log(hash: &Hash) { |
| 69 | + crate::pubkey::log(hash); |
| 70 | +} |
| 71 | + |
| 72 | +/// Reads the entry count from the first 8 bytes of data. |
| 73 | +/// Returns None if the data is too short. |
| 74 | +#[inline(always)] |
| 75 | +pub(crate) fn read_entry_count_from_bytes(data: &[u8]) -> Option<usize> { |
| 76 | + if data.len() < NUM_ENTRIES_SIZE { |
| 77 | + return None; |
| 78 | + } |
| 79 | + Some(unsafe { |
| 80 | + // SAFETY: `data` is guaranteed to be at least `NUM_ENTRIES_SIZE` bytes long by the |
| 81 | + // preceding length check, so it is sound to read the first 8 bytes and interpret |
| 82 | + // them as a little-endian `u64`. |
| 83 | + u64::from_le_bytes(*(data.as_ptr() as *const [u8; NUM_ENTRIES_SIZE])) |
| 84 | + } as usize) |
| 85 | +} |
| 86 | + |
| 87 | +/// Reads the entry count from the first 8 bytes of data. |
| 88 | +/// |
| 89 | +/// # Safety |
| 90 | +/// Caller must ensure data has at least `NUM_ENTRIES_SIZE` bytes. |
| 91 | +#[inline(always)] |
| 92 | +pub(crate) unsafe fn read_entry_count_from_bytes_unchecked(data: &[u8]) -> usize { |
| 93 | + u64::from_le_bytes(*(data.as_ptr() as *const [u8; NUM_ENTRIES_SIZE])) as usize |
| 94 | +} |
| 95 | + |
| 96 | +/// Validates `SlotHashes` data format. |
| 97 | +/// |
| 98 | +/// The function checks: |
| 99 | +/// 1. The buffer is large enough to contain the entry count. |
| 100 | +/// 2. The buffer length is sufficient to hold the declared number of entries. |
| 101 | +/// |
| 102 | +/// It returns `Ok(())` if the data is well-formed, otherwise an appropriate |
| 103 | +/// `ProgramError` describing the issue. |
| 104 | +#[inline] |
| 105 | +fn parse_and_validate_data(data: &[u8]) -> Result<(), ProgramError> { |
| 106 | + if data.len() < NUM_ENTRIES_SIZE { |
| 107 | + return Err(ProgramError::AccountDataTooSmall); |
| 108 | + } |
| 109 | + |
| 110 | + // SAFETY: We've confirmed that data has enough bytes to read the entry count. |
| 111 | + let num_entries = unsafe { read_entry_count_from_bytes_unchecked(data) }; |
| 112 | + |
| 113 | + let min_size = NUM_ENTRIES_SIZE + num_entries * ENTRY_SIZE; |
| 114 | + if data.len() < min_size { |
| 115 | + return Err(ProgramError::AccountDataTooSmall); |
| 116 | + } |
| 117 | + |
| 118 | + Ok(()) |
| 119 | +} |
| 120 | + |
| 121 | +impl SlotHashEntry { |
| 122 | + /// Returns the slot number as a `u64`. |
| 123 | + #[inline(always)] |
| 124 | + pub fn slot(&self) -> Slot { |
| 125 | + u64::from_le_bytes(self.slot_le) |
| 126 | + } |
| 127 | +} |
| 128 | + |
| 129 | +impl<T: Deref<Target = [u8]>> SlotHashes<T> { |
| 130 | + /// Creates a `SlotHashes` instance with validation of the entry count and buffer size. |
| 131 | + /// |
| 132 | + /// This constructor validates that the buffer has at least enough bytes to contain |
| 133 | + /// the declared number of entries. The buffer can be any size above the minimum required, |
| 134 | + /// making it suitable for both full `MAX_SIZE` buffers and smaller test data. |
| 135 | + /// Does not validate that entries are sorted in descending order. |
| 136 | + #[inline(always)] |
| 137 | + pub fn new(data: T) -> Result<Self, ProgramError> { |
| 138 | + parse_and_validate_data(&data)?; |
| 139 | + // SAFETY: `parse_and_validate_data` verifies that the data slice has at least |
| 140 | + // `NUM_ENTRIES_SIZE` bytes for the entry count and enough additional bytes to |
| 141 | + // contain the declared number of entries, thus upholding all invariants required |
| 142 | + // by `SlotHashes::new_unchecked`. |
| 143 | + Ok(unsafe { Self::new_unchecked(data) }) |
| 144 | + } |
| 145 | + |
| 146 | + /// Creates a `SlotHashes` instance without validation. |
| 147 | + /// |
| 148 | + /// This is an unsafe constructor that bypasses all validation checks for performance. |
| 149 | + /// In debug builds, it still runs `parse_and_validate_data` as a sanity check. |
| 150 | + /// |
| 151 | + /// # Safety |
| 152 | + /// |
| 153 | + /// This function is unsafe because it does not validate the data size or format. |
| 154 | + /// The caller must ensure: |
| 155 | + /// 1. The underlying byte slice in `data` represents valid `SlotHashes` data |
| 156 | + /// (length prefix plus entries, where entries are sorted in descending order by slot). |
| 157 | + /// 2. The data slice has at least `NUM_ENTRIES_SIZE + (declared_entries * ENTRY_SIZE)` bytes. |
| 158 | + /// 3. The first 8 bytes contain a valid entry count in little-endian format. |
| 159 | + /// |
| 160 | + #[inline(always)] |
| 161 | + pub unsafe fn new_unchecked(data: T) -> Self { |
| 162 | + if cfg!(debug_assertions) { |
| 163 | + parse_and_validate_data(&data) |
| 164 | + .expect("`data` matches all the same requirements as for `new()`"); |
| 165 | + } |
| 166 | + |
| 167 | + SlotHashes { data } |
| 168 | + } |
| 169 | + |
| 170 | + /// Returns the number of `SlotHashEntry` items accessible. |
| 171 | + #[inline(always)] |
| 172 | + pub fn len(&self) -> usize { |
| 173 | + // SAFETY: `SlotHashes::new` and `new_unchecked` guarantee that `self.data` has at |
| 174 | + // least `NUM_ENTRIES_SIZE` bytes, so reading the entry count without additional |
| 175 | + // checks is safe. |
| 176 | + unsafe { read_entry_count_from_bytes_unchecked(&self.data) } |
| 177 | + } |
| 178 | + |
| 179 | + /// Returns if the sysvar is empty. |
| 180 | + #[inline(always)] |
| 181 | + pub fn is_empty(&self) -> bool { |
| 182 | + self.len() == 0 |
| 183 | + } |
| 184 | + |
| 185 | + /// Returns a `&[SlotHashEntry]` view into the underlying data. |
| 186 | + /// |
| 187 | + /// Call once and reuse the slice if you need many look-ups. |
| 188 | + /// |
| 189 | + /// The constructor (in the safe path that called `parse_and_validate_data`) |
| 190 | + /// or caller (if unsafe `new_unchecked` path) is responsible for ensuring |
| 191 | + /// the slice is big enough and properly aligned. |
| 192 | + #[inline(always)] |
| 193 | + pub fn entries(&self) -> &[SlotHashEntry] { |
| 194 | + unsafe { |
| 195 | + // SAFETY: The slice begins `NUM_ENTRIES_SIZE` bytes into `self.data`, which |
| 196 | + // is guaranteed by parse_and_validate_data() to have at least `len * ENTRY_SIZE` |
| 197 | + // additional bytes. The pointer is properly aligned for `SlotHashEntry` (which |
| 198 | + // a compile-time assertion ensures is alignment 1). |
| 199 | + from_raw_parts( |
| 200 | + self.data.as_ptr().add(NUM_ENTRIES_SIZE) as *const SlotHashEntry, |
| 201 | + self.len(), |
| 202 | + ) |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + /// Gets a reference to the entry at `index` or `None` if out of bounds. |
| 207 | + #[inline(always)] |
| 208 | + pub fn get_entry(&self, index: usize) -> Option<&SlotHashEntry> { |
| 209 | + if index >= self.len() { |
| 210 | + return None; |
| 211 | + } |
| 212 | + Some(unsafe { self.get_entry_unchecked(index) }) |
| 213 | + } |
| 214 | + |
| 215 | + /// Finds the hash for a specific slot using binary search. |
| 216 | + /// |
| 217 | + /// Returns the hash if the slot is found, or `None` if not found. |
| 218 | + /// Assumes entries are sorted by slot in descending order. |
| 219 | + /// If calling repeatedly, prefer getting `entries()` in caller |
| 220 | + /// to avoid repeated slice construction. |
| 221 | + #[inline(always)] |
| 222 | + pub fn get_hash(&self, target_slot: Slot) -> Option<&Hash> { |
| 223 | + self.position(target_slot) |
| 224 | + .map(|index| unsafe { &self.get_entry_unchecked(index).hash }) |
| 225 | + } |
| 226 | + |
| 227 | + /// Finds the position (index) of a specific slot using binary search. |
| 228 | + /// |
| 229 | + /// Returns the index if the slot is found, or `None` if not found. |
| 230 | + /// Assumes entries are sorted by slot in descending order. |
| 231 | + /// If calling repeatedly, prefer getting `entries()` in caller |
| 232 | + /// to avoid repeated slice construction. |
| 233 | + #[inline(always)] |
| 234 | + pub fn position(&self, target_slot: Slot) -> Option<usize> { |
| 235 | + self.entries() |
| 236 | + .binary_search_by(|probe_entry| probe_entry.slot().cmp(&target_slot).reverse()) |
| 237 | + .ok() |
| 238 | + } |
| 239 | + |
| 240 | + /// Returns a reference to the entry at `index` **without** bounds checking. |
| 241 | + /// |
| 242 | + /// # Safety |
| 243 | + /// Caller must guarantee that `index < self.len()`. |
| 244 | + #[inline(always)] |
| 245 | + pub unsafe fn get_entry_unchecked(&self, index: usize) -> &SlotHashEntry { |
| 246 | + debug_assert!(index < self.len()); |
| 247 | + // SAFETY: Caller guarantees `index < self.len()`. The data pointer is valid |
| 248 | + // and aligned for `SlotHashEntry`. The offset calculation points to a |
| 249 | + // valid entry within the allocated data. |
| 250 | + let entries_ptr = self.data.as_ptr().add(NUM_ENTRIES_SIZE) as *const SlotHashEntry; |
| 251 | + &*entries_ptr.add(index) |
| 252 | + } |
| 253 | +} |
| 254 | + |
| 255 | +impl<'a, T: Deref<Target = [u8]>> IntoIterator for &'a SlotHashes<T> { |
| 256 | + type Item = &'a SlotHashEntry; |
| 257 | + type IntoIter = core::slice::Iter<'a, SlotHashEntry>; |
| 258 | + |
| 259 | + fn into_iter(self) -> Self::IntoIter { |
| 260 | + self.entries().iter() |
| 261 | + } |
| 262 | +} |
| 263 | + |
| 264 | +impl<'a> SlotHashes<Ref<'a, [u8]>> { |
| 265 | + /// Creates a `SlotHashes` instance by safely borrowing data from an `AccountInfo`. |
| 266 | + /// |
| 267 | + /// This function verifies that: |
| 268 | + /// - The account key matches the `SLOTHASHES_ID` |
| 269 | + /// - The account data can be successfully borrowed |
| 270 | + /// |
| 271 | + /// Returns a `SlotHashes` instance that borrows the account's data for zero-copy access. |
| 272 | + /// The returned instance is valid for the lifetime of the borrow. |
| 273 | + /// |
| 274 | + /// # Errors |
| 275 | + /// - `ProgramError::InvalidArgument` if the account key doesn't match the `SlotHashes` sysvar ID |
| 276 | + /// - `ProgramError::AccountBorrowFailed` if the account data is already mutably borrowed |
| 277 | + #[inline(always)] |
| 278 | + pub fn from_account_info(account_info: &'a AccountInfo) -> Result<Self, ProgramError> { |
| 279 | + if account_info.key() != &SLOTHASHES_ID { |
| 280 | + return Err(ProgramError::InvalidArgument); |
| 281 | + } |
| 282 | + |
| 283 | + let data_ref = account_info.try_borrow_data()?; |
| 284 | + |
| 285 | + // SAFETY: The account was validated to be the `SlotHashes` sysvar. |
| 286 | + Ok(unsafe { SlotHashes::new_unchecked(data_ref) }) |
| 287 | + } |
| 288 | +} |
| 289 | + |
| 290 | +#[cfg(feature = "std")] |
| 291 | +impl SlotHashes<Box<[u8]>> { |
| 292 | + /// Fills the provided buffer with the full `SlotHashes` sysvar data. |
| 293 | + /// |
| 294 | + /// # Safety |
| 295 | + /// The caller must ensure the buffer pointer is valid for `MAX_SIZE` bytes. |
| 296 | + /// The syscall will write exactly `MAX_SIZE` bytes to the buffer. |
| 297 | + #[inline(always)] |
| 298 | + unsafe fn fill_from_sysvar(buffer_ptr: *mut u8) -> Result<(), ProgramError> { |
| 299 | + crate::sysvars::get_sysvar_unchecked(buffer_ptr, &SLOTHASHES_ID, 0, MAX_SIZE)?; |
| 300 | + |
| 301 | + // For tests on builds that don't actually fill the buffer. |
| 302 | + #[cfg(not(target_os = "solana"))] |
| 303 | + core::ptr::write_bytes(buffer_ptr, 0, NUM_ENTRIES_SIZE); |
| 304 | + |
| 305 | + Ok(()) |
| 306 | + } |
| 307 | + |
| 308 | + /// Allocates an optimal buffer for the sysvar data based on available features. |
| 309 | + #[inline(always)] |
| 310 | + fn allocate_and_fetch() -> Result<Box<[u8]>, ProgramError> { |
| 311 | + let mut buf = std::vec::Vec::with_capacity(MAX_SIZE); |
| 312 | + unsafe { |
| 313 | + // SAFETY: `buf` was allocated with capacity `MAX_SIZE` so its |
| 314 | + // pointer is valid for exactly that many bytes. `fill_from_sysvar` |
| 315 | + // writes `MAX_SIZE` bytes, and we immediately set the length to |
| 316 | + // `MAX_SIZE`, marking the entire buffer as initialized before it is |
| 317 | + // turned into a boxed slice. |
| 318 | + Self::fill_from_sysvar(buf.as_mut_ptr())?; |
| 319 | + buf.set_len(MAX_SIZE); |
| 320 | + } |
| 321 | + Ok(buf.into_boxed_slice()) |
| 322 | + } |
| 323 | + |
| 324 | + /// Fetches the `SlotHashes` sysvar data directly via syscall. This copies |
| 325 | + /// the full sysvar data (`MAX_SIZE` bytes). |
| 326 | + #[inline(always)] |
| 327 | + pub fn fetch() -> Result<Self, ProgramError> { |
| 328 | + let data_init = Self::allocate_and_fetch()?; |
| 329 | + |
| 330 | + // SAFETY: The data was initialized by the syscall. |
| 331 | + Ok(unsafe { SlotHashes::new_unchecked(data_init) }) |
| 332 | + } |
| 333 | +} |
0 commit comments