1use crate::error::ProgramError;
34use crate::sysvar::{get_sysvar_prefix_at, SLOT_HASHES_ID};
35
36pub const WINDOW: usize = 16;
39
40pub const ENTRY_LEN: usize = 40;
42
43pub const MAX_ENTRIES: usize = 512;
45
46const HEADER_LEN: usize = 8;
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum SlotHashStatus {
51 Found([u8; 32]),
53 Skipped,
56 TooOld,
58 Ahead,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub struct SlotHashLookup {
66 pub status: SlotHashStatus,
67 pub reads: u8,
69}
70
71impl SlotHashLookup {
72 #[inline(always)]
74 pub const fn hash(&self) -> Option<[u8; 32]> {
75 match self.status {
76 SlotHashStatus::Found(hash) => Some(hash),
77 _ => None,
78 }
79 }
80}
81
82#[inline]
85pub fn slot_hash(slot: u64) -> Result<Option<[u8; 32]>, ProgramError> {
86 Ok(slot_hash_lookup(slot)?.hash())
87}
88
89#[inline]
91pub fn slot_hash_lookup(slot: u64) -> Result<SlotHashLookup, ProgramError> {
92 slot_hash_lookup_with(slot, |offset, dst| {
93 get_sysvar_prefix_at(&SLOT_HASHES_ID, offset, dst)
94 })
95}
96
97#[inline(always)]
98fn entry_slot(window: &[u8], index: usize) -> u64 {
99 let at = index * ENTRY_LEN;
100 u64::from_le_bytes([
101 window[at],
102 window[at + 1],
103 window[at + 2],
104 window[at + 3],
105 window[at + 4],
106 window[at + 5],
107 window[at + 6],
108 window[at + 7],
109 ])
110}
111
112#[inline(always)]
113fn entry_hash(window: &[u8], index: usize) -> [u8; 32] {
114 let at = index * ENTRY_LEN + 8;
115 let mut hash = [0u8; 32];
116 hash.copy_from_slice(&window[at..at + 32]);
117 hash
118}
119
120pub fn slot_hash_lookup_with<R>(target: u64, mut read: R) -> Result<SlotHashLookup, ProgramError>
127where
128 R: FnMut(u64, &mut [u8]) -> Result<bool, ProgramError>,
129{
130 let mut reads = 0u8;
131 let done = |status, reads| Ok(SlotHashLookup { status, reads });
132
133 let mut head = [0u8; HEADER_LEN + WINDOW * ENTRY_LEN];
136 reads += 1;
137 let count = if read(0, &mut head)? {
138 read_count(&head)?
139 } else {
140 reads += 1;
141 if !read(0, &mut head[..HEADER_LEN])? {
142 return Err(ProgramError::UnsupportedSysvar);
143 }
144 let count = read_count(&head)?;
145 if count >= WINDOW {
146 return Err(ProgramError::UnsupportedSysvar);
149 }
150 if count > 0 {
151 reads += 1;
152 if !read(0, &mut head[..HEADER_LEN + count * ENTRY_LEN])? {
153 return Err(ProgramError::UnsupportedSysvar);
154 }
155 }
156 count
157 };
158 if count == 0 {
159 return done(SlotHashStatus::Ahead, reads);
160 }
161
162 let have = if count < WINDOW { count } else { WINDOW };
163 let window = &head[HEADER_LEN..HEADER_LEN + have * ENTRY_LEN];
164 let newest = entry_slot(window, 0);
165 if target > newest {
166 return done(SlotHashStatus::Ahead, reads);
167 }
168 if let Some(status) = scan(window, have, target, true) {
169 return done(status, reads);
170 }
171 if have == count {
173 return done(SlotHashStatus::TooOld, reads);
174 }
175
176 let last = count - 1;
181 let bound = newest - target;
182 let mut hi = if bound > last as u64 {
184 last
185 } else {
186 bound as usize
187 };
188 let mut lo = have;
190 let mut newer = (have - 1, entry_slot(window, have - 1));
193 let mut older: Option<(usize, u64)> = None;
194 let mut buffer = [0u8; WINDOW * ENTRY_LEN];
195 let mut bisect = false;
196 while lo <= hi {
197 let span = hi - lo + 1;
198 let width = if span < WINDOW { span } else { WINDOW };
199 let highest_start = hi + 1 - width;
200 let start = match older {
201 None => highest_start,
204 Some(_) if bisect => lo + (span - width) / 2,
207 Some((older_index, older_slot)) => {
210 let gap = (older_index - newer.0) as u128;
211 let run = (newer.1 - older_slot) as u128;
212 let guess = newer.0 + ((newer.1 - target) as u128 * gap / run) as usize;
213 let centred = guess.saturating_sub(width / 2);
214 if centred < lo {
215 lo
216 } else if centred > highest_start {
217 highest_start
218 } else {
219 centred
220 }
221 }
222 };
223 bisect = older.is_some() && !bisect;
224 let window = &mut buffer[..width * ENTRY_LEN];
225 reads = reads.saturating_add(1);
226 let offset = (HEADER_LEN + start * ENTRY_LEN) as u64;
227 if !read(offset, window)? {
228 return Err(ProgramError::UnsupportedSysvar);
229 }
230 if let Some(status) = scan(window, width, target, start == lo) {
233 return done(status, reads);
234 }
235 let first_slot = entry_slot(window, 0);
236 if first_slot < target {
237 older = Some((start, first_slot));
239 hi = start - 1;
240 } else {
241 let end = start + width - 1;
243 newer = (end, entry_slot(window, width - 1));
244 lo = end + 1;
245 }
246 }
247 if older.is_some() {
252 done(SlotHashStatus::Skipped, reads)
253 } else {
254 done(SlotHashStatus::TooOld, reads)
255 }
256}
257
258#[inline(always)]
259fn read_count(head: &[u8]) -> Result<usize, ProgramError> {
260 let count = u64::from_le_bytes([
261 head[0], head[1], head[2], head[3], head[4], head[5], head[6], head[7],
262 ]);
263 if count > MAX_ENTRIES as u64 {
264 return Err(ProgramError::UnsupportedSysvar);
265 }
266 Ok(count as usize)
267}
268
269#[inline(always)]
275fn scan(window: &[u8], len: usize, target: u64, newer_before: bool) -> Option<SlotHashStatus> {
276 let mut i = 0;
277 while i < len {
278 let slot = entry_slot(window, i);
279 if slot == target {
280 return Some(SlotHashStatus::Found(entry_hash(window, i)));
281 }
282 if slot < target {
283 return if i > 0 || newer_before {
284 Some(SlotHashStatus::Skipped)
285 } else {
286 None
287 };
288 }
289 i += 1;
290 }
291 None
292}
293
294#[cfg(test)]
295mod tests {
296 extern crate std;
297
298 use super::*;
299 use std::vec::Vec;
300
301 fn hash_of(slot: u64) -> [u8; 32] {
302 let mut hash = [0u8; 32];
303 hash[..8].copy_from_slice(&slot.to_le_bytes());
304 hash[8..16].copy_from_slice(&(!slot).to_le_bytes());
305 hash[31] = 0x5a;
306 hash
307 }
308
309 fn image(slots: &[u64]) -> Vec<u8> {
311 let mut out = (slots.len() as u64).to_le_bytes().to_vec();
312 for slot in slots {
313 out.extend_from_slice(&slot.to_le_bytes());
314 out.extend_from_slice(&hash_of(*slot));
315 }
316 out
317 }
318
319 fn lookup(image: &[u8], target: u64) -> SlotHashLookup {
320 let mut reads = 0u8;
321 let result = slot_hash_lookup_with(target, |offset, dst| {
322 reads += 1;
323 let start = offset as usize;
324 match start.checked_add(dst.len()) {
325 Some(end) if end <= image.len() => {
326 dst.copy_from_slice(&image[start..end]);
327 Ok(true)
328 }
329 _ => Ok(false),
330 }
331 })
332 .unwrap();
333 assert_eq!(result.reads, reads, "the lookup counts its own reads");
334 result
335 }
336
337 fn expected(slots: &[u64], target: u64) -> SlotHashStatus {
339 match slots.first() {
340 None => SlotHashStatus::Ahead,
341 Some(newest) if target > *newest => SlotHashStatus::Ahead,
342 _ if slots.contains(&target) => SlotHashStatus::Found(hash_of(target)),
343 _ if target < *slots.last().unwrap() => SlotHashStatus::TooOld,
344 _ => SlotHashStatus::Skipped,
345 }
346 }
347
348 fn chain(newest: u64, count: usize, mut skip: impl FnMut(u64) -> bool) -> Vec<u64> {
351 let mut slots = Vec::new();
352 let mut slot = newest;
353 while slots.len() < count {
354 if slots.is_empty() || !skip(slot) {
355 slots.push(slot);
356 }
357 if slot == 0 {
358 break;
359 }
360 slot -= 1;
361 }
362 slots
363 }
364
365 fn check_every_slot(slots: &[u64]) -> u8 {
366 let sysvar = image(slots);
367 let newest = slots.first().copied().unwrap_or(0);
368 let oldest = slots.last().copied().unwrap_or(0);
369 let mut worst = 0;
370 let low = oldest.saturating_sub(3);
371 for target in low..=newest + 3 {
372 let got = lookup(&sysvar, target);
373 assert_eq!(
374 got.status,
375 expected(slots, target),
376 "target {target} in a list of {} from {newest} to {oldest}",
377 slots.len()
378 );
379 worst = worst.max(got.reads);
380 }
381 worst
382 }
383
384 #[test]
385 fn a_full_sysvar_with_no_skips_answers_in_two_reads() {
386 let slots = chain(1_000_000, MAX_ENTRIES, |_| false);
387 let sysvar = image(&slots);
388 assert_eq!(sysvar.len(), 20_488);
389 for back in 0..WINDOW as u64 {
391 let got = lookup(&sysvar, 1_000_000 - back);
392 assert_eq!(got.hash(), Some(hash_of(1_000_000 - back)));
393 assert_eq!(got.reads, 1);
394 }
395 for back in WINDOW as u64..MAX_ENTRIES as u64 {
397 let got = lookup(&sysvar, 1_000_000 - back);
398 assert_eq!(got.hash(), Some(hash_of(1_000_000 - back)));
399 assert_eq!(got.reads, 2, "{back} slots back");
400 }
401 assert_eq!(check_every_slot(&slots), 2);
402 }
403
404 #[test]
405 fn skipped_slots_are_reported_and_cost_little() {
406 let slots = chain(5_000_000, MAX_ENTRIES, |slot| slot % 19 == 7);
408 let worst = check_every_slot(&slots);
409 assert!(worst <= 4, "{worst} reads in the worst case");
410 let sysvar = image(&slots);
411 let skipped = (4_999_900..5_000_000u64).find(|s| s % 19 == 7).unwrap();
412 assert_eq!(lookup(&sysvar, skipped).status, SlotHashStatus::Skipped);
413 assert_eq!(lookup(&sysvar, 5_000_000 - 100).reads, 2);
415 }
416
417 #[test]
418 fn long_gaps_fall_back_to_the_search() {
419 let slots = chain(9_000, MAX_ENTRIES, |slot| slot % 3 != 0);
421 let worst = check_every_slot(&slots);
422 assert!(worst <= 8, "{worst} reads in the worst case");
423 let slots = chain(80_000, MAX_ENTRIES, |slot| (79_500..79_800).contains(&slot));
425 let worst = check_every_slot(&slots);
426 assert!(worst <= 8, "{worst} reads in the worst case");
427 }
428
429 #[test]
430 fn every_length_from_empty_to_full() {
431 for count in 0..=40usize {
432 let slots = chain(700, count, |slot| slot % 5 == 1);
433 check_every_slot(&slots);
434 }
435 for count in [63, 64, 65, 255, 256, 257, 511, 512] {
436 let slots = chain(90_000, count, |slot| slot % 11 == 3);
437 check_every_slot(&slots);
438 }
439 let slots = chain(30, 31, |_| false);
441 assert_eq!(*slots.last().unwrap(), 0);
442 check_every_slot(&slots);
443 }
444
445 #[test]
446 fn the_reasons_are_told_apart() {
447 let slots = chain(1_000, 100, |slot| slot == 950);
448 let sysvar = image(&slots);
449 let oldest = *slots.last().unwrap();
450 assert_eq!(lookup(&sysvar, 1_001).status, SlotHashStatus::Ahead);
451 assert_eq!(lookup(&sysvar, u64::MAX).status, SlotHashStatus::Ahead);
452 assert_eq!(lookup(&sysvar, 950).status, SlotHashStatus::Skipped);
453 assert_eq!(lookup(&sysvar, oldest - 1).status, SlotHashStatus::TooOld);
454 assert_eq!(lookup(&sysvar, 0).status, SlotHashStatus::TooOld);
455 assert_eq!(lookup(&sysvar, oldest).hash(), Some(hash_of(oldest)));
456 assert_eq!(lookup(&image(&[]), 5).status, SlotHashStatus::Ahead);
457 }
458
459 #[test]
460 fn a_header_that_does_not_describe_the_sysvar_is_refused() {
461 let mut sysvar = image(&chain(100, 20, |_| false));
463 sysvar[..8].copy_from_slice(&513u64.to_le_bytes());
464 let result = slot_hash_lookup_with(90, |offset, dst| {
465 let start = offset as usize;
466 if start + dst.len() > sysvar.len() {
467 return Ok(false);
468 }
469 dst.copy_from_slice(&sysvar[start..start + dst.len()]);
470 Ok(true)
471 });
472 assert_eq!(result.err(), Some(ProgramError::UnsupportedSysvar));
473
474 let mut sysvar = image(&chain(100, 20, |_| false));
476 sysvar[..8].copy_from_slice(&400u64.to_le_bytes());
477 let result = slot_hash_lookup_with(60, |offset, dst| {
478 let start = offset as usize;
479 if start + dst.len() > sysvar.len() {
480 return Ok(false);
481 }
482 dst.copy_from_slice(&sysvar[start..start + dst.len()]);
483 Ok(true)
484 });
485 assert_eq!(result.err(), Some(ProgramError::UnsupportedSysvar));
486
487 let result = slot_hash_lookup_with(60, |_, _| Err(ProgramError::InvalidArgument));
489 assert_eq!(result.err(), Some(ProgramError::InvalidArgument));
490 }
491}