use {
super::test_utils::*,
crate::{
account::{AccountView, RuntimeAccount},
error::ProgramError,
sysvars::{clock::Slot, slot_hashes::*},
},
alloc::vec::Vec,
core::{
mem::{align_of, size_of},
ptr,
},
};
#[test]
fn test_layout_constants() {
assert_eq!(NUM_ENTRIES_SIZE, size_of::<u64>());
assert_eq!(SLOT_SIZE, size_of::<u64>());
assert_eq!(HASH_BYTES, 32);
assert_eq!(ENTRY_SIZE, size_of::<u64>() + 32);
assert_eq!(MAX_SIZE, 20_488);
assert_eq!(size_of::<SlotHashEntry>(), ENTRY_SIZE);
assert_eq!(align_of::<SlotHashEntry>(), align_of::<[u8; 8]>());
assert!(
SLOTHASHES_ID
== Address::new_from_array([
6, 167, 213, 23, 25, 47, 10, 175, 198, 242, 101, 227, 251, 119, 204, 122, 218, 130,
197, 41, 208, 190, 59, 19, 110, 45, 0, 85, 32, 0, 0, 0,
])
);
pub fn check_base58(input_bytes: &[u8], expected_b58: &str) {
assert_eq!(
Address::from_str_const(expected_b58).as_array(),
input_bytes
);
}
check_base58(
SLOTHASHES_ID.as_array(),
"SysvarS1otHashes111111111111111111111111111",
);
}
#[test]
fn test_binary_search_no_std() {
const TEST_NUM_ENTRIES: usize = 512;
const START_SLOT: u64 = 2000;
let entries =
generate_mock_entries(TEST_NUM_ENTRIES, START_SLOT, DecrementStrategy::Average1_05);
let data = create_mock_data(&entries);
let entry_count = entries.len();
let first_slot = entries[0].0;
let mid_index = entry_count / 2;
let mid_slot = entries[mid_index].0;
let last_slot = entries[entry_count - 1].0;
let slot_hashes = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
assert_eq!(slot_hashes.position(first_slot), Some(0));
let expected_mid_index = Some(mid_index);
let actual_pos_mid = slot_hashes.position(mid_slot);
let start_idx = mid_index.saturating_sub(2);
let end_idx = core::cmp::min(entry_count, mid_index.saturating_add(3));
let surrounding_slots: Vec<_> = entries[start_idx..end_idx].iter().map(|e| e.0).collect();
assert_eq!(
actual_pos_mid, expected_mid_index,
"position({}) failed! Surrounding slots: {:?}",
mid_slot, surrounding_slots
);
assert_eq!(slot_hashes.position(last_slot), Some(entry_count - 1));
assert_eq!(slot_hashes.position(START_SLOT + 1), None);
let mut missing_internal_slot = None;
for i in 0..(entries.len() - 1) {
if entries[i].0 > entries[i + 1].0 + 1 {
missing_internal_slot = Some(entries[i + 1].0 + 1);
break;
}
}
assert!(
missing_internal_slot.is_some(),
"Test requires at least one gap between slots"
);
assert_eq!(slot_hashes.position(missing_internal_slot.unwrap()), None);
assert_eq!(slot_hashes.get_hash(first_slot), Some(&entries[0].1));
assert_eq!(slot_hashes.get_hash(mid_slot), Some(&entries[mid_index].1));
assert_eq!(
slot_hashes.get_hash(last_slot),
Some(&entries[entry_count - 1].1)
);
assert_eq!(slot_hashes.get_hash(START_SLOT + 1), None);
let empty_entries = generate_mock_entries(0, START_SLOT, DecrementStrategy::Strictly1);
let empty_data = create_mock_data(&empty_entries);
let empty_hashes = unsafe { SlotHashes::new_unchecked(empty_data.as_slice()) };
assert_eq!(empty_hashes.get_hash(100), None);
let pos_start_plus_1 = slot_hashes.position(START_SLOT + 1);
assert!(
pos_start_plus_1.is_none(),
"position(START_SLOT + 1) should be None"
);
}
#[test]
fn test_basic_getters_and_iterator_no_std() {
const NUM_ENTRIES: usize = 512;
const START_SLOT: u64 = 2000;
let entries = generate_mock_entries(NUM_ENTRIES, START_SLOT, DecrementStrategy::Strictly1);
let data = create_mock_data(&entries);
let slot_hashes = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
assert_eq!(slot_hashes.len(), NUM_ENTRIES);
let entry0 = slot_hashes.get_entry(0);
assert!(entry0.is_some());
assert_eq!(entry0.unwrap().slot(), START_SLOT); assert_eq!(entry0.unwrap().hash, [0u8; HASH_BYTES]);
let entry2 = slot_hashes.get_entry(NUM_ENTRIES - 1); assert!(entry2.is_some());
assert_eq!(entry2.unwrap().slot(), entries[NUM_ENTRIES - 1].0);
assert_eq!(entry2.unwrap().hash, entries[NUM_ENTRIES - 1].1);
assert!(slot_hashes.get_entry(NUM_ENTRIES).is_none());
for (i, entry) in slot_hashes.into_iter().enumerate() {
assert_eq!(entry.slot(), entries[i].0);
assert_eq!(entry.hash, entries[i].1);
}
assert!(slot_hashes.into_iter().nth(NUM_ENTRIES).is_none());
let mut iter_hint = slot_hashes.into_iter();
assert_eq!(iter_hint.len(), NUM_ENTRIES);
iter_hint.next();
assert_eq!(iter_hint.len(), NUM_ENTRIES - 1);
for _ in 1..NUM_ENTRIES {
iter_hint.next();
}
iter_hint.next();
assert_eq!(iter_hint.len(), 0);
let empty_data = create_mock_data(&[]);
let empty_hashes = unsafe { SlotHashes::new_unchecked(empty_data.as_slice()) };
assert_eq!(empty_hashes.len(), 0);
assert!(empty_hashes.get_entry(0).is_none());
assert!(empty_hashes.into_iter().next().is_none());
}
#[test]
fn test_entry_count_no_std() {
let entries: &[(Slot, Hash)] = &[(100, [1u8; HASH_BYTES]), (98, [2u8; HASH_BYTES])];
let data = create_mock_data(entries);
let slot_hashes = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
assert_eq!(slot_hashes.len(), 2);
let num_entries = entries.len() as u64;
let data_len = NUM_ENTRIES_SIZE + entries.len() * ENTRY_SIZE;
let mut small_data = alloc::vec![0u8; data_len];
small_data[0..NUM_ENTRIES_SIZE].copy_from_slice(&num_entries.to_le_bytes());
let mut offset = NUM_ENTRIES_SIZE;
for (slot, hash) in entries {
small_data[offset..offset + SLOT_SIZE].copy_from_slice(&slot.to_le_bytes());
small_data[offset + SLOT_SIZE..offset + ENTRY_SIZE].copy_from_slice(hash);
offset += ENTRY_SIZE;
}
let res1 = SlotHashes::new(small_data.as_slice());
assert!(
res1.is_ok(),
"SlotHashes::new should succeed with a correctly sized buffer"
);
let slot_hashes_from_small = res1.unwrap();
assert_eq!(slot_hashes_from_small.len(), entries.len());
let empty_data = create_mock_data(&[]);
let empty_hashes = unsafe { SlotHashes::new_unchecked(empty_data.as_slice()) };
assert_eq!(empty_hashes.len(), 0);
}
#[test]
fn test_get_entry_unchecked_no_std() {
let single_entry: &[(Slot, Hash)] = &[(100, [1u8; HASH_BYTES])];
let data = create_mock_data(single_entry);
let slot_hashes = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
let entry = unsafe { slot_hashes.get_entry_unchecked(0) };
assert_eq!(entry.slot(), 100);
assert_eq!(entry.hash, [1u8; HASH_BYTES]);
}
#[test]
fn test_get_entry_unchecked_last_no_std() {
const COUNT: usize = 8;
const START_SLOT: u64 = 600;
let entries = generate_mock_entries(COUNT, START_SLOT, DecrementStrategy::Strictly1);
let data = create_mock_data(&entries);
let sh = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
let last = unsafe { sh.get_entry_unchecked(COUNT - 1) };
assert_eq!(last.slot(), entries[COUNT - 1].0);
assert_eq!(last.hash, entries[COUNT - 1].1);
}
#[test]
fn test_iterator_into_ref_no_std() {
const NUM: usize = 16;
const START: u64 = 100;
let entries = generate_mock_entries(NUM, START, DecrementStrategy::Strictly1);
let data = create_mock_data(&entries);
let sh = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
let mut sum: u64 = 0;
for e in &sh {
sum += e.slot();
}
let expected_sum: u64 = entries.iter().map(|(s, _)| *s).sum();
assert_eq!(sum, expected_sum);
let iter = (&sh).into_iter();
assert_eq!(iter.len(), sh.len());
}
#[test]
fn test_mock_data_max_entries_boundary() {
let entries = generate_mock_entries(MAX_ENTRIES, 1000, DecrementStrategy::Strictly1);
let data = create_mock_data(&entries);
let sh = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
assert_eq!(sh.len(), MAX_ENTRIES);
}
#[test]
fn test_mock_data_raw_byte_layout() {
let entries = &[(100u64, [0xAB; 32])];
let data = create_mock_data(entries);
assert_eq!(&data[0..8], &1u64.to_le_bytes());
assert_eq!(&data[8..16], &100u64.to_le_bytes());
assert_eq!(&data[16..48], &[0xAB; 32]);
}
#[test]
fn test_read_entry_count_from_bytes() {
let entry_count = 42u64;
let mut data = [0u8; 16];
data[0..8].copy_from_slice(&entry_count.to_le_bytes());
let result = unsafe { get_entry_count(&data) };
assert_eq!(result, 42);
let zero_count = 0u64;
let mut zero_data = [0u8; 8];
zero_data.copy_from_slice(&zero_count.to_le_bytes());
let zero_result = unsafe { get_entry_count(&zero_data) };
assert_eq!(zero_result, 0);
let max_count = MAX_ENTRIES as u64;
let mut max_data = [0u8; 8];
max_data.copy_from_slice(&max_count.to_le_bytes());
let max_result = unsafe { get_entry_count(&max_data) };
assert_eq!(max_result, MAX_ENTRIES);
}
fn mock_fetch_into_unchecked(
mock_sysvar_data: &[u8],
buffer: &mut [u8],
offset: u64,
) -> Result<(), ProgramError> {
let offset = offset as usize;
if offset >= mock_sysvar_data.len() {
return Err(ProgramError::InvalidArgument);
}
let available_len = mock_sysvar_data.len() - offset;
let copy_len = core::cmp::min(buffer.len(), available_len);
buffer[..copy_len].copy_from_slice(&mock_sysvar_data[offset..offset + copy_len]);
Ok(())
}
#[test]
fn test_mock_offset_copy() {
let entries = &[
(100u64, [1u8; HASH_BYTES]),
(99u64, [2u8; HASH_BYTES]),
(98u64, [3u8; HASH_BYTES]),
];
let mock_sysvar_data = create_mock_data(entries);
let mut buffer_full = alloc::vec![0u8; mock_sysvar_data.len()];
mock_fetch_into_unchecked(&mock_sysvar_data, &mut buffer_full, 0).unwrap();
assert_eq!(buffer_full, mock_sysvar_data);
let entries_size = 3 * ENTRY_SIZE;
let mut buffer_entries = alloc::vec![0u8; entries_size];
mock_fetch_into_unchecked(&mock_sysvar_data, &mut buffer_entries, 8).unwrap();
assert_eq!(buffer_entries, &mock_sysvar_data[8..8 + entries_size]);
let remaining_entries_size = 2 * ENTRY_SIZE;
let mut buffer_skip_first = alloc::vec![0u8; remaining_entries_size];
let skip_first_offset = 8 + ENTRY_SIZE;
mock_fetch_into_unchecked(
&mock_sysvar_data,
&mut buffer_skip_first,
skip_first_offset as u64,
)
.unwrap();
assert_eq!(
buffer_skip_first,
&mock_sysvar_data[skip_first_offset..skip_first_offset + remaining_entries_size]
);
let mut small_buffer = [0u8; 16]; mock_fetch_into_unchecked(&mock_sysvar_data, &mut small_buffer, 0).unwrap();
assert_eq!(small_buffer, &mock_sysvar_data[0..16]);
let mut buffer_beyond = [0u8; 10];
let beyond_offset = mock_sysvar_data.len() as u64;
assert!(
mock_fetch_into_unchecked(&mock_sysvar_data, &mut buffer_beyond, beyond_offset).is_err()
);
}
#[test]
fn test_entries_exposed_no_std() {
let entries = generate_mock_entries(8, 80, DecrementStrategy::Strictly1);
let data = create_mock_data(&entries);
let sh = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
let slice = sh.entries();
assert_eq!(slice.len(), entries.len());
for (i, e) in slice.iter().enumerate() {
assert_eq!(e.slot(), entries[i].0);
assert_eq!(e.hash, entries[i].1);
}
}
#[test]
fn test_safe_vs_unsafe_getters_consistency() {
let entries = generate_mock_entries(16, 200, DecrementStrategy::Strictly1);
let data = create_mock_data(&entries);
let sh = unsafe { SlotHashes::new_unchecked(data.as_slice()) };
for i in 0..entries.len() {
let safe_entry = sh.get_entry(i).unwrap();
let unsafe_entry = unsafe { sh.get_entry_unchecked(i) };
assert_eq!(safe_entry, unsafe_entry);
}
assert_eq!(sh.len(), entries.len());
}
#[test]
fn test_entry_count_header_too_short() {
let short = [0u8; 4];
assert!(SlotHashes::new(&short[..]).is_err());
}
#[test]
fn test_log_function() {
let test_hash: Hash = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32,
];
log(&test_hash);
}
#[test]
fn test_from_account_view_constructor() {
const NUM_ENTRIES: usize = 3;
const START_SLOT: u64 = 1234;
let mock_entries = generate_mock_entries(NUM_ENTRIES, START_SLOT, DecrementStrategy::Strictly1);
let data = create_mock_data(&mock_entries);
let mut aligned_backing: Vec<u64>;
let acct_ptr = unsafe {
let header_size = core::mem::size_of::<AccountLayout>();
let total_size = header_size + data.len();
let word_len = total_size.div_ceil(8);
aligned_backing = alloc::vec![0u64; word_len];
let base_ptr = aligned_backing.as_mut_ptr() as *mut u8;
let header_ptr = base_ptr as *mut AccountLayout;
ptr::write(
header_ptr,
AccountLayout {
borrow_state: crate::entrypoint::NON_DUP_MARKER,
is_signer: 0,
is_writable: 0,
executable: 0,
resize_delta: 0,
key: SLOTHASHES_ID,
owner: Address::new_from_array([0u8; 32]),
lamports: 0,
data_len: data.len() as u64,
},
);
ptr::copy_nonoverlapping(data.as_ptr(), base_ptr.add(header_size), data.len());
base_ptr as *mut RuntimeAccount
};
let account_view = unsafe { AccountView::new_unchecked(acct_ptr) };
let slot_hashes = SlotHashes::from_account_view(&account_view)
.expect("from_account_view should succeed with well-formed data");
assert_eq!(slot_hashes.len(), NUM_ENTRIES);
for (i, entry) in slot_hashes.into_iter().enumerate() {
assert_eq!(entry.slot(), mock_entries[i].0);
assert_eq!(entry.hash, mock_entries[i].1);
}
}
#[cfg(feature = "alloc")]
#[test]
fn test_fetch_allocates_buffer_host() {
const START_SLOT: u64 = 500;
let entries = generate_mock_entries(5, START_SLOT, DecrementStrategy::Strictly1);
let data = create_mock_data(&entries);
let mut slot_hashes =
SlotHashes::<alloc::boxed::Box<[u8]>>::fetch().expect("fetch() should allocate");
slot_hashes.data[..data.len()].copy_from_slice(&data);
assert_eq!(slot_hashes.len(), entries.len());
for (i, entry) in slot_hashes.into_iter().enumerate() {
assert_eq!(entry.slot(), entries[i].0);
assert_eq!(entry.hash, entries[i].1);
}
}