use crate::error::ProgramError;
use crate::sysvar::{get_sysvar_prefix_at, SLOT_HASHES_ID};
pub const WINDOW: usize = 16;
pub const ENTRY_LEN: usize = 40;
pub const MAX_ENTRIES: usize = 512;
const HEADER_LEN: usize = 8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SlotHashStatus {
Found([u8; 32]),
Skipped,
TooOld,
Ahead,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SlotHashLookup {
pub status: SlotHashStatus,
pub reads: u8,
}
impl SlotHashLookup {
#[inline(always)]
pub const fn hash(&self) -> Option<[u8; 32]> {
match self.status {
SlotHashStatus::Found(hash) => Some(hash),
_ => None,
}
}
}
#[inline]
pub fn slot_hash(slot: u64) -> Result<Option<[u8; 32]>, ProgramError> {
Ok(slot_hash_lookup(slot)?.hash())
}
#[inline]
pub fn slot_hash_lookup(slot: u64) -> Result<SlotHashLookup, ProgramError> {
slot_hash_lookup_with(slot, |offset, dst| {
get_sysvar_prefix_at(&SLOT_HASHES_ID, offset, dst)
})
}
#[inline(always)]
fn entry_slot(window: &[u8], index: usize) -> u64 {
let at = index * ENTRY_LEN;
u64::from_le_bytes([
window[at],
window[at + 1],
window[at + 2],
window[at + 3],
window[at + 4],
window[at + 5],
window[at + 6],
window[at + 7],
])
}
#[inline(always)]
fn entry_hash(window: &[u8], index: usize) -> [u8; 32] {
let at = index * ENTRY_LEN + 8;
let mut hash = [0u8; 32];
hash.copy_from_slice(&window[at..at + 32]);
hash
}
pub fn slot_hash_lookup_with<R>(target: u64, mut read: R) -> Result<SlotHashLookup, ProgramError>
where
R: FnMut(u64, &mut [u8]) -> Result<bool, ProgramError>,
{
let mut reads = 0u8;
let done = |status, reads| Ok(SlotHashLookup { status, reads });
let mut head = [0u8; HEADER_LEN + WINDOW * ENTRY_LEN];
reads += 1;
let count = if read(0, &mut head)? {
read_count(&head)?
} else {
reads += 1;
if !read(0, &mut head[..HEADER_LEN])? {
return Err(ProgramError::UnsupportedSysvar);
}
let count = read_count(&head)?;
if count >= WINDOW {
return Err(ProgramError::UnsupportedSysvar);
}
if count > 0 {
reads += 1;
if !read(0, &mut head[..HEADER_LEN + count * ENTRY_LEN])? {
return Err(ProgramError::UnsupportedSysvar);
}
}
count
};
if count == 0 {
return done(SlotHashStatus::Ahead, reads);
}
let have = if count < WINDOW { count } else { WINDOW };
let window = &head[HEADER_LEN..HEADER_LEN + have * ENTRY_LEN];
let newest = entry_slot(window, 0);
if target > newest {
return done(SlotHashStatus::Ahead, reads);
}
if let Some(status) = scan(window, have, target, true) {
return done(status, reads);
}
if have == count {
return done(SlotHashStatus::TooOld, reads);
}
let last = count - 1;
let bound = newest - target;
let mut hi = if bound > last as u64 {
last
} else {
bound as usize
};
let mut lo = have;
let mut newer = (have - 1, entry_slot(window, have - 1));
let mut older: Option<(usize, u64)> = None;
let mut buffer = [0u8; WINDOW * ENTRY_LEN];
let mut bisect = false;
while lo <= hi {
let span = hi - lo + 1;
let width = if span < WINDOW { span } else { WINDOW };
let highest_start = hi + 1 - width;
let start = match older {
None => highest_start,
Some(_) if bisect => lo + (span - width) / 2,
Some((older_index, older_slot)) => {
let gap = (older_index - newer.0) as u128;
let run = (newer.1 - older_slot) as u128;
let guess = newer.0 + ((newer.1 - target) as u128 * gap / run) as usize;
let centred = guess.saturating_sub(width / 2);
if centred < lo {
lo
} else if centred > highest_start {
highest_start
} else {
centred
}
}
};
bisect = older.is_some() && !bisect;
let window = &mut buffer[..width * ENTRY_LEN];
reads = reads.saturating_add(1);
let offset = (HEADER_LEN + start * ENTRY_LEN) as u64;
if !read(offset, window)? {
return Err(ProgramError::UnsupportedSysvar);
}
if let Some(status) = scan(window, width, target, start == lo) {
return done(status, reads);
}
let first_slot = entry_slot(window, 0);
if first_slot < target {
older = Some((start, first_slot));
hi = start - 1;
} else {
let end = start + width - 1;
newer = (end, entry_slot(window, width - 1));
lo = end + 1;
}
}
if older.is_some() {
done(SlotHashStatus::Skipped, reads)
} else {
done(SlotHashStatus::TooOld, reads)
}
}
#[inline(always)]
fn read_count(head: &[u8]) -> Result<usize, ProgramError> {
let count = u64::from_le_bytes([
head[0], head[1], head[2], head[3], head[4], head[5], head[6], head[7],
]);
if count > MAX_ENTRIES as u64 {
return Err(ProgramError::UnsupportedSysvar);
}
Ok(count as usize)
}
#[inline(always)]
fn scan(window: &[u8], len: usize, target: u64, newer_before: bool) -> Option<SlotHashStatus> {
let mut i = 0;
while i < len {
let slot = entry_slot(window, i);
if slot == target {
return Some(SlotHashStatus::Found(entry_hash(window, i)));
}
if slot < target {
return if i > 0 || newer_before {
Some(SlotHashStatus::Skipped)
} else {
None
};
}
i += 1;
}
None
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use std::vec::Vec;
fn hash_of(slot: u64) -> [u8; 32] {
let mut hash = [0u8; 32];
hash[..8].copy_from_slice(&slot.to_le_bytes());
hash[8..16].copy_from_slice(&(!slot).to_le_bytes());
hash[31] = 0x5a;
hash
}
fn image(slots: &[u64]) -> Vec<u8> {
let mut out = (slots.len() as u64).to_le_bytes().to_vec();
for slot in slots {
out.extend_from_slice(&slot.to_le_bytes());
out.extend_from_slice(&hash_of(*slot));
}
out
}
fn lookup(image: &[u8], target: u64) -> SlotHashLookup {
let mut reads = 0u8;
let result = slot_hash_lookup_with(target, |offset, dst| {
reads += 1;
let start = offset as usize;
match start.checked_add(dst.len()) {
Some(end) if end <= image.len() => {
dst.copy_from_slice(&image[start..end]);
Ok(true)
}
_ => Ok(false),
}
})
.unwrap();
assert_eq!(result.reads, reads, "the lookup counts its own reads");
result
}
fn expected(slots: &[u64], target: u64) -> SlotHashStatus {
match slots.first() {
None => SlotHashStatus::Ahead,
Some(newest) if target > *newest => SlotHashStatus::Ahead,
_ if slots.contains(&target) => SlotHashStatus::Found(hash_of(target)),
_ if target < *slots.last().unwrap() => SlotHashStatus::TooOld,
_ => SlotHashStatus::Skipped,
}
}
fn chain(newest: u64, count: usize, mut skip: impl FnMut(u64) -> bool) -> Vec<u64> {
let mut slots = Vec::new();
let mut slot = newest;
while slots.len() < count {
if slots.is_empty() || !skip(slot) {
slots.push(slot);
}
if slot == 0 {
break;
}
slot -= 1;
}
slots
}
fn check_every_slot(slots: &[u64]) -> u8 {
let sysvar = image(slots);
let newest = slots.first().copied().unwrap_or(0);
let oldest = slots.last().copied().unwrap_or(0);
let mut worst = 0;
let low = oldest.saturating_sub(3);
for target in low..=newest + 3 {
let got = lookup(&sysvar, target);
assert_eq!(
got.status,
expected(slots, target),
"target {target} in a list of {} from {newest} to {oldest}",
slots.len()
);
worst = worst.max(got.reads);
}
worst
}
#[test]
fn a_full_sysvar_with_no_skips_answers_in_two_reads() {
let slots = chain(1_000_000, MAX_ENTRIES, |_| false);
let sysvar = image(&slots);
assert_eq!(sysvar.len(), 20_488);
for back in 0..WINDOW as u64 {
let got = lookup(&sysvar, 1_000_000 - back);
assert_eq!(got.hash(), Some(hash_of(1_000_000 - back)));
assert_eq!(got.reads, 1);
}
for back in WINDOW as u64..MAX_ENTRIES as u64 {
let got = lookup(&sysvar, 1_000_000 - back);
assert_eq!(got.hash(), Some(hash_of(1_000_000 - back)));
assert_eq!(got.reads, 2, "{back} slots back");
}
assert_eq!(check_every_slot(&slots), 2);
}
#[test]
fn skipped_slots_are_reported_and_cost_little() {
let slots = chain(5_000_000, MAX_ENTRIES, |slot| slot % 19 == 7);
let worst = check_every_slot(&slots);
assert!(worst <= 4, "{worst} reads in the worst case");
let sysvar = image(&slots);
let skipped = (4_999_900..5_000_000u64).find(|s| s % 19 == 7).unwrap();
assert_eq!(lookup(&sysvar, skipped).status, SlotHashStatus::Skipped);
assert_eq!(lookup(&sysvar, 5_000_000 - 100).reads, 2);
}
#[test]
fn long_gaps_fall_back_to_the_search() {
let slots = chain(9_000, MAX_ENTRIES, |slot| slot % 3 != 0);
let worst = check_every_slot(&slots);
assert!(worst <= 8, "{worst} reads in the worst case");
let slots = chain(80_000, MAX_ENTRIES, |slot| (79_500..79_800).contains(&slot));
let worst = check_every_slot(&slots);
assert!(worst <= 8, "{worst} reads in the worst case");
}
#[test]
fn every_length_from_empty_to_full() {
for count in 0..=40usize {
let slots = chain(700, count, |slot| slot % 5 == 1);
check_every_slot(&slots);
}
for count in [63, 64, 65, 255, 256, 257, 511, 512] {
let slots = chain(90_000, count, |slot| slot % 11 == 3);
check_every_slot(&slots);
}
let slots = chain(30, 31, |_| false);
assert_eq!(*slots.last().unwrap(), 0);
check_every_slot(&slots);
}
#[test]
fn the_reasons_are_told_apart() {
let slots = chain(1_000, 100, |slot| slot == 950);
let sysvar = image(&slots);
let oldest = *slots.last().unwrap();
assert_eq!(lookup(&sysvar, 1_001).status, SlotHashStatus::Ahead);
assert_eq!(lookup(&sysvar, u64::MAX).status, SlotHashStatus::Ahead);
assert_eq!(lookup(&sysvar, 950).status, SlotHashStatus::Skipped);
assert_eq!(lookup(&sysvar, oldest - 1).status, SlotHashStatus::TooOld);
assert_eq!(lookup(&sysvar, 0).status, SlotHashStatus::TooOld);
assert_eq!(lookup(&sysvar, oldest).hash(), Some(hash_of(oldest)));
assert_eq!(lookup(&image(&[]), 5).status, SlotHashStatus::Ahead);
}
#[test]
fn a_header_that_does_not_describe_the_sysvar_is_refused() {
let mut sysvar = image(&chain(100, 20, |_| false));
sysvar[..8].copy_from_slice(&513u64.to_le_bytes());
let result = slot_hash_lookup_with(90, |offset, dst| {
let start = offset as usize;
if start + dst.len() > sysvar.len() {
return Ok(false);
}
dst.copy_from_slice(&sysvar[start..start + dst.len()]);
Ok(true)
});
assert_eq!(result.err(), Some(ProgramError::UnsupportedSysvar));
let mut sysvar = image(&chain(100, 20, |_| false));
sysvar[..8].copy_from_slice(&400u64.to_le_bytes());
let result = slot_hash_lookup_with(60, |offset, dst| {
let start = offset as usize;
if start + dst.len() > sysvar.len() {
return Ok(false);
}
dst.copy_from_slice(&sysvar[start..start + dst.len()]);
Ok(true)
});
assert_eq!(result.err(), Some(ProgramError::UnsupportedSysvar));
let result = slot_hash_lookup_with(60, |_, _| Err(ProgramError::InvalidArgument));
assert_eq!(result.err(), Some(ProgramError::InvalidArgument));
}
}