use core::mem::MaybeUninit;
use crate::address::Address;
use crate::error::ProgramError;
pub const MAX_SEGMENT_BORROWS: usize = 16;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(u8)]
pub enum AccessKind {
Read = 0,
Write = 1,
}
#[inline(always)]
fn address_fingerprint(address: &Address) -> u64 {
let bytes = address.as_array();
u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])
}
#[inline(always)]
fn address_eq(a: &Address, b: &Address) -> bool {
a.as_array() == b.as_array()
}
#[inline(always)]
fn borrow_eq(a: &SegmentBorrow, b: &SegmentBorrow) -> bool {
a.key_fp == b.key_fp
&& address_eq(&a.key, &b.key)
&& a.offset == b.offset
&& a.size == b.size
&& a.kind == b.kind
}
#[derive(Clone, Copy, Debug)]
pub struct SegmentBorrow {
pub key_fp: u64,
pub key: Address,
pub offset: u32,
pub size: u32,
pub kind: AccessKind,
}
#[inline(always)]
const fn ranges_overlap(a_off: u32, a_size: u32, b_off: u32, b_size: u32) -> bool {
let a_end = a_off as u64 + a_size as u64;
let b_end = b_off as u64 + b_size as u64;
!(a_end <= b_off as u64 || b_end <= a_off as u64)
}
pub struct SegmentBorrowRegistry {
entries: [MaybeUninit<SegmentBorrow>; MAX_SEGMENT_BORROWS],
len: u8,
}
#[cfg(feature = "touch-map")]
pub const MAX_TOUCH_RECORDS: usize = 32;
#[cfg(feature = "touch-map")]
pub const TOUCH_MAP_MAGIC: u8 = 0x7A;
#[cfg(feature = "touch-map")]
pub const TOUCH_MAP_VERSION: u8 = 0x01;
#[cfg(feature = "touch-map")]
pub const TOUCH_MAP_FLAG_OVERFLOWED: u8 = 1 << 0;
#[cfg(feature = "touch-map")]
pub const TOUCH_MAP_FLAG_SKIPPED: u8 = 1 << 1;
#[cfg(feature = "touch-map")]
pub const TOUCH_MAP_HEADER_LEN: usize = 4;
#[cfg(feature = "touch-map")]
pub const TOUCH_MAP_RECORD_LEN: usize = 9;
#[cfg(feature = "touch-map")]
pub const TOUCH_MAP_MAX_ENCODED_LEN: usize =
TOUCH_MAP_HEADER_LEN + MAX_TOUCH_RECORDS * TOUCH_MAP_RECORD_LEN;
#[cfg(feature = "touch-map")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TouchMapRecord {
pub slot: u8,
pub offset: u32,
pub size: u32,
pub write: bool,
}
#[cfg(feature = "touch-map")]
pub fn encode_touch_map(
records: &[TouchMapRecord],
overflowed: bool,
skipped: bool,
) -> ([u8; TOUCH_MAP_MAX_ENCODED_LEN], usize) {
let mut buf = [0u8; TOUCH_MAP_MAX_ENCODED_LEN];
let mut flags = 0u8;
if overflowed {
flags |= TOUCH_MAP_FLAG_OVERFLOWED;
}
if skipped {
flags |= TOUCH_MAP_FLAG_SKIPPED;
}
let mut count = 0usize;
let mut pos = TOUCH_MAP_HEADER_LEN;
for rec in records {
if count >= MAX_TOUCH_RECORDS {
flags |= TOUCH_MAP_FLAG_OVERFLOWED;
break;
}
if rec.offset > i32::MAX as u32 {
flags |= TOUCH_MAP_FLAG_SKIPPED;
continue;
}
let packed = rec.offset | if rec.write { 0x8000_0000 } else { 0 };
buf[pos] = rec.slot;
buf[pos + 1..pos + 5].copy_from_slice(&packed.to_le_bytes());
buf[pos + 5..pos + 9].copy_from_slice(&rec.size.to_le_bytes());
pos += TOUCH_MAP_RECORD_LEN;
count += 1;
}
buf[0] = TOUCH_MAP_MAGIC;
buf[1] = TOUCH_MAP_VERSION;
buf[2] = flags;
buf[3] = count as u8;
(buf, pos)
}
#[cfg(feature = "touch-map")]
pub(crate) mod touch_log {
use super::{
address_eq, address_fingerprint, borrow_eq, AccessKind, SegmentBorrow, MAX_TOUCH_RECORDS,
};
use crate::address::Address;
pub(crate) fn merge_exact(a: &SegmentBorrow, b: &SegmentBorrow) -> Option<SegmentBorrow> {
if a.key_fp != b.key_fp || !address_eq(&a.key, &b.key) {
return None;
}
let a_end = a.offset as u64 + a.size as u64;
let b_end = b.offset as u64 + b.size as u64;
if a.kind == b.kind {
if b.offset as u64 > a_end || a.offset as u64 > b_end {
return None;
}
let offset = if a.offset < b.offset {
a.offset
} else {
b.offset
};
let end = if a_end > b_end { a_end } else { b_end };
let size = end - offset as u64;
if size > u32::MAX as u64 {
return None;
}
let mut merged = *a;
merged.offset = offset;
merged.size = size as u32;
return Some(merged);
}
let (read, write) = if a.kind == AccessKind::Write {
(b, a)
} else {
(a, b)
};
let read_end = read.offset as u64 + read.size as u64;
let write_end = write.offset as u64 + write.size as u64;
if write.offset <= read.offset && read_end <= write_end {
return Some(*write);
}
None
}
#[repr(C)]
pub(crate) struct TouchLog {
len: u8,
overflow: u8,
_pad: [u8; 6],
entries: [SegmentBorrow; MAX_TOUCH_RECORDS],
}
impl TouchLog {
#[cfg_attr(target_os = "solana", allow(dead_code))]
pub(crate) const fn new() -> Self {
const ZERO: SegmentBorrow = SegmentBorrow {
key_fp: 0,
key: Address::new_from_array([0u8; 32]),
offset: 0,
size: 0,
kind: AccessKind::Read,
};
Self {
len: 0,
overflow: 0,
_pad: [0; 6],
entries: [ZERO; MAX_TOUCH_RECORDS],
}
}
fn record(&mut self, borrow: &SegmentBorrow) {
let len = self.len as usize;
let mut i = 0;
while i < len {
if borrow_eq(&self.entries[i], borrow) {
return;
}
i += 1;
}
if len >= MAX_TOUCH_RECORDS {
self.record_under_pressure(borrow);
return;
}
self.entries[len] = *borrow;
self.len = (len + 1) as u8;
}
fn record_under_pressure(&mut self, borrow: &SegmentBorrow) {
let len = self.len as usize;
let mut i = len;
while i > 0 {
i -= 1;
if let Some(merged) = merge_exact(&self.entries[i], borrow) {
self.entries[i] = merged;
return;
}
}
if self.overflow != 0 {
return;
}
if self.compact() {
let len = self.len as usize;
self.entries[len] = *borrow;
self.len = (len + 1) as u8;
return;
}
self.overflow = 1;
}
fn compact(&mut self) -> bool {
let before = self.len;
loop {
let mut merged_any = false;
let mut i = 0;
while i < self.len as usize {
let mut j = i + 1;
while j < self.len as usize {
if let Some(merged) = merge_exact(&self.entries[i], &self.entries[j]) {
self.entries[i] = merged;
self.remove_at(j);
merged_any = true;
} else {
j += 1;
}
}
i += 1;
}
if !merged_any {
return self.len < before;
}
}
}
fn remove_at(&mut self, idx: usize) {
let len = self.len as usize;
let mut k = idx;
while k + 1 < len {
self.entries[k] = self.entries[k + 1];
k += 1;
}
self.len = (len - 1) as u8;
}
fn for_each<F: FnMut(&SegmentBorrow)>(&self, mut f: F) {
let len = self.len as usize;
let mut i = 0;
while i < len {
f(&self.entries[i]);
i += 1;
}
}
fn reset(&mut self) {
self.len = 0;
self.overflow = 0;
}
}
#[cfg(test)]
pub(crate) fn assert_all_zero_is_the_valid_empty_log(zeroed_backing: &[u64]) {
assert!(zeroed_backing.len() * 8 >= core::mem::size_of::<TouchLog>());
assert!(zeroed_backing.iter().all(|&w| w == 0));
let overlaid = unsafe { &*(zeroed_backing.as_ptr() as *const TouchLog) };
assert_eq!(overlaid.len, 0, "zeroed heap must read as the empty log");
assert_eq!(overlaid.overflow, 0);
let fresh = TouchLog::new();
assert_eq!(fresh.len, 0);
assert_eq!(fresh.overflow, 0);
assert!(fresh._pad.iter().all(|&b| b == 0));
let mut i = 0;
while i < MAX_TOUCH_RECORDS {
let e = &fresh.entries[i];
assert!(e.key_fp == 0 && e.offset == 0 && e.size == 0);
assert!(matches!(e.kind, AccessKind::Read), "0 must decode as Read");
assert!(e.key.as_array().iter().all(|&b| b == 0));
i += 1;
}
}
#[inline]
pub(crate) fn record(borrow: &SegmentBorrow) {
with_log(|log| log.record(borrow));
}
#[inline]
pub(crate) fn record_account(key: &Address, data_len: u32, kind: AccessKind) {
let borrow = SegmentBorrow {
key_fp: address_fingerprint(key),
key: *key,
offset: 0,
size: data_len,
kind,
};
record(&borrow);
}
#[inline]
pub(crate) fn for_each<F: FnMut(&SegmentBorrow)>(f: F) {
with_log(|log| log.for_each(f));
}
#[inline]
pub(crate) fn len() -> usize {
with_log(|log| log.len as usize)
}
#[inline]
pub(crate) fn overflowed() -> bool {
with_log(|log| log.overflow != 0)
}
#[inline]
pub(crate) fn reset() {
with_log(|log| log.reset());
}
#[cfg(target_os = "solana")]
mod store {
use super::TouchLog;
const TOUCH_HEAP_OFFSET: usize = (crate::write_policy::SBF_GATE_HEAP_END + 7) & !7;
const _: () = assert!(
TOUCH_HEAP_OFFSET + core::mem::size_of::<TouchLog>()
<= hopper_native::HEAP_RUNTIME_RESERVED,
"TouchLog exceeds HEAP_RUNTIME_RESERVED; grow the reservation in \
hopper-native/src/entrypoint.rs or shrink MAX_TOUCH_RECORDS"
);
const _: () = assert!((hopper_native::HEAP_START_ADDRESS + TOUCH_HEAP_OFFSET) % 8 == 0);
pub(super) fn with_log<R>(f: impl FnOnce(&mut TouchLog) -> R) -> R {
let ptr = (hopper_native::HEAP_START_ADDRESS + TOUCH_HEAP_OFFSET) as *mut TouchLog;
f(unsafe { &mut *ptr })
}
}
#[cfg(all(
not(target_os = "solana"),
any(test, feature = "thread-local-registry")
))]
mod store {
use super::TouchLog;
use std::cell::RefCell;
std::thread_local! {
static LOG: RefCell<TouchLog> = const { RefCell::new(TouchLog::new()) };
}
pub(super) fn with_log<R>(f: impl FnOnce(&mut TouchLog) -> R) -> R {
LOG.with(|cell| f(&mut cell.borrow_mut()))
}
}
#[cfg(all(
not(target_os = "solana"),
not(any(test, feature = "thread-local-registry"))
))]
mod store {
use super::TouchLog;
use core::cell::UnsafeCell;
use core::sync::atomic::{AtomicBool, Ordering};
struct SpinlockTouchLog {
lock: AtomicBool,
cell: UnsafeCell<TouchLog>,
}
unsafe impl Sync for SpinlockTouchLog {}
static LOG: SpinlockTouchLog = SpinlockTouchLog {
lock: AtomicBool::new(false),
cell: UnsafeCell::new(TouchLog::new()),
};
pub(super) fn with_log<R>(f: impl FnOnce(&mut TouchLog) -> R) -> R {
while LOG
.lock
.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
core::hint::spin_loop();
}
let result = f(unsafe { &mut *LOG.cell.get() });
LOG.lock.store(false, Ordering::Release);
result
}
}
use store::with_log;
}
impl Default for SegmentBorrowRegistry {
#[inline(always)]
fn default() -> Self {
Self::new()
}
}
impl SegmentBorrowRegistry {
#[inline(always)]
pub const fn new() -> Self {
const EMPTY: MaybeUninit<SegmentBorrow> = MaybeUninit::uninit();
Self {
entries: [EMPTY; MAX_SEGMENT_BORROWS],
len: 0,
}
}
#[cfg(feature = "touch-map")]
#[inline]
fn record_touch(&mut self, borrow: &SegmentBorrow) {
touch_log::record(borrow);
}
#[cfg(feature = "touch-map")]
#[inline]
pub fn record_account_touch(&mut self, key: &Address, data_len: u32, kind: AccessKind) {
touch_log::record_account(key, data_len, kind);
}
#[cfg(feature = "touch-map")]
#[inline]
pub fn for_each_touch<F: FnMut(&SegmentBorrow)>(&self, f: F) {
touch_log::for_each(f);
}
#[cfg(feature = "touch-map")]
#[inline(always)]
pub fn touch_map_len(&self) -> usize {
touch_log::len()
}
#[cfg(feature = "touch-map")]
#[inline(always)]
pub fn touch_map_overflowed(&self) -> bool {
touch_log::overflowed()
}
#[inline(always)]
pub const fn len(&self) -> usize {
self.len as usize
}
#[inline(always)]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[inline(always)]
pub fn register_leased_read(
&mut self,
key: &Address,
offset: u32,
size: u32,
) -> Result<SegmentBorrow, ProgramError> {
let borrow = SegmentBorrow {
key_fp: address_fingerprint(key),
key: *key,
offset,
size,
kind: AccessKind::Read,
};
self.register(borrow)?;
Ok(borrow)
}
#[inline(always)]
pub fn register_leased_write(
&mut self,
key: &Address,
offset: u32,
size: u32,
) -> Result<SegmentBorrow, ProgramError> {
let borrow = SegmentBorrow {
key_fp: address_fingerprint(key),
key: *key,
offset,
size,
kind: AccessKind::Write,
};
self.register(borrow)?;
Ok(borrow)
}
#[inline(always)]
pub fn register(&mut self, new: SegmentBorrow) -> Result<(), ProgramError> {
let len = self.len as usize;
if len >= MAX_SEGMENT_BORROWS {
return Err(ProgramError::AccountBorrowFailed);
}
let mut i = 0;
while i < len {
let existing = unsafe { self.entries.get_unchecked(i).assume_init_ref() };
if existing.key_fp == new.key_fp
&& address_eq(&existing.key, &new.key)
&& ranges_overlap(existing.offset, existing.size, new.offset, new.size)
{
match (existing.kind, new.kind) {
(AccessKind::Read, AccessKind::Read) => {}
_ => return Err(ProgramError::AccountBorrowFailed),
}
}
i += 1;
}
unsafe { self.entries.get_unchecked_mut(len).write(new) };
self.len = (len + 1) as u8;
#[cfg(feature = "touch-map")]
self.record_touch(&new);
Ok(())
}
#[inline(always)]
pub fn register_read(
&mut self,
key: &Address,
offset: u32,
size: u32,
) -> Result<(), ProgramError> {
self.register(SegmentBorrow {
key_fp: address_fingerprint(key),
key: *key,
offset,
size,
kind: AccessKind::Read,
})
}
#[inline(always)]
pub fn register_write(
&mut self,
key: &Address,
offset: u32,
size: u32,
) -> Result<(), ProgramError> {
self.register(SegmentBorrow {
key_fp: address_fingerprint(key),
key: *key,
offset,
size,
kind: AccessKind::Write,
})
}
#[inline(always)]
pub fn release(&mut self, borrow: &SegmentBorrow) -> bool {
let len = self.len as usize;
let mut i = 0;
while i < len {
let existing = unsafe { self.entries.get_unchecked(i).assume_init_ref() };
if borrow_eq(existing, borrow) {
let new_len = len - 1;
self.len = new_len as u8;
if i < new_len {
let last = unsafe { self.entries.get_unchecked(new_len).assume_init() };
unsafe { self.entries.get_unchecked_mut(i).write(last) };
}
return true;
}
i += 1;
}
false
}
#[doc(hidden)]
#[inline(always)]
pub unsafe fn release_last_registered(&mut self, borrow: &SegmentBorrow) -> bool {
let len = self.len as usize;
if len == 0 {
return false;
}
let last = unsafe { *self.entries.get_unchecked(len - 1).assume_init_ref() };
if !borrow_eq(&last, borrow) {
return self.release(borrow);
}
self.len = (len - 1) as u8;
true
}
#[inline(always)]
pub fn clear(&mut self) {
self.len = 0;
}
#[inline(always)]
pub fn would_conflict(&self, proposed: &SegmentBorrow) -> bool {
let len = self.len as usize;
let mut i = 0;
while i < len {
let existing = unsafe { self.entries.get_unchecked(i).assume_init_ref() };
if existing.key_fp == proposed.key_fp
&& address_eq(&existing.key, &proposed.key)
&& ranges_overlap(
existing.offset,
existing.size,
proposed.offset,
proposed.size,
)
{
match (existing.kind, proposed.kind) {
(AccessKind::Read, AccessKind::Read) => {}
_ => return true,
}
}
i += 1;
}
false
}
#[inline(always)]
pub fn register_guard(
&mut self,
borrow: SegmentBorrow,
) -> Result<SegmentBorrowGuard<'_>, ProgramError> {
self.register(borrow)?;
Ok(SegmentBorrowGuard {
registry: self,
borrow,
})
}
#[inline(always)]
pub fn register_guard_read(
&mut self,
key: &Address,
offset: u32,
size: u32,
) -> Result<SegmentBorrowGuard<'_>, ProgramError> {
let borrow = SegmentBorrow {
key_fp: address_fingerprint(key),
key: *key,
offset,
size,
kind: AccessKind::Read,
};
self.register_guard(borrow)
}
#[inline(always)]
pub fn register_guard_write(
&mut self,
key: &Address,
offset: u32,
size: u32,
) -> Result<SegmentBorrowGuard<'_>, ProgramError> {
let borrow = SegmentBorrow {
key_fp: address_fingerprint(key),
key: *key,
offset,
size,
kind: AccessKind::Write,
};
self.register_guard(borrow)
}
#[inline]
pub fn for_each<F: FnMut(&SegmentBorrow)>(&self, mut f: F) {
let len = self.len as usize;
let mut i = 0;
while i < len {
f(unsafe { self.entries.get_unchecked(i).assume_init_ref() });
i += 1;
}
}
#[inline]
pub fn find_exact(
&self,
key: &Address,
offset: u32,
size: u32,
kind: AccessKind,
) -> Option<&SegmentBorrow> {
let fp = address_fingerprint(key);
let len = self.len as usize;
let mut i = 0;
while i < len {
let e = unsafe { self.entries.get_unchecked(i).assume_init_ref() };
if e.key_fp == fp
&& address_eq(&e.key, key)
&& e.offset == offset
&& e.size == size
&& e.kind == kind
{
return Some(e);
}
i += 1;
}
None
}
}
pub struct SegmentBorrowGuard<'a> {
registry: &'a mut SegmentBorrowRegistry,
borrow: SegmentBorrow,
}
impl<'a> SegmentBorrowGuard<'a> {
#[inline(always)]
pub fn kind(&self) -> AccessKind {
self.borrow.kind
}
#[inline(always)]
pub fn offset(&self) -> u32 {
self.borrow.offset
}
#[inline(always)]
pub fn size(&self) -> u32 {
self.borrow.size
}
}
impl<'a> Drop for SegmentBorrowGuard<'a> {
fn drop(&mut self) {
self.registry.release(&self.borrow);
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
fn range_overlap_is_symmetric_for_arbitrary_u32s() {
let a_off: u32 = kani::any();
let a_size: u32 = kani::any();
let b_off: u32 = kani::any();
let b_size: u32 = kani::any();
assert_eq!(
ranges_overlap(a_off, a_size, b_off, b_size),
ranges_overlap(b_off, b_size, a_off, a_size)
);
}
#[kani::proof]
fn overlapping_write_blocks_same_account_accesses() {
let offset: u32 = kani::any();
let size: u32 = kani::any();
let delta: u32 = kani::any();
kani::assume(offset <= 1024);
kani::assume(size > 0 && size <= 64);
kani::assume(delta < size);
let key = Address::new([7u8; 32]);
let probe_offset = offset + delta;
let mut reg = SegmentBorrowRegistry::new();
assert!(reg.register_write(&key, offset, size).is_ok());
assert!(reg.register_read(&key, probe_offset, 1).is_err());
assert!(reg.register_write(&key, probe_offset, 1).is_err());
assert_eq!(reg.len(), 1);
}
#[kani::proof]
fn overlapping_reads_are_shared_for_same_account() {
let offset: u32 = kani::any();
let size: u32 = kani::any();
let delta: u32 = kani::any();
kani::assume(offset <= 1024);
kani::assume(size > 0 && size <= 64);
kani::assume(delta < size);
let key = Address::new([8u8; 32]);
let probe_offset = offset + delta;
let mut reg = SegmentBorrowRegistry::new();
assert!(reg.register_read(&key, offset, size).is_ok());
assert!(reg.register_read(&key, probe_offset, 1).is_ok());
assert_eq!(reg.len(), 2);
}
#[kani::proof]
fn fingerprint_collision_different_addresses_do_not_conflict() {
let key_a = Address::new([9u8; 32]);
let mut key_b_bytes = [9u8; 32];
key_b_bytes[8] = 10;
let key_b = Address::new(key_b_bytes);
let mut reg = SegmentBorrowRegistry::new();
assert_eq!(address_fingerprint(&key_a), address_fingerprint(&key_b));
assert_ne!(key_a.as_array(), key_b.as_array());
assert!(reg.register_write(&key_a, 0, 8).is_ok());
assert!(reg.register_write(&key_b, 0, 8).is_ok());
assert_eq!(reg.len(), 2);
}
#[kani::proof]
fn release_removes_exact_borrow_and_preserves_others() {
let key = Address::new([11u8; 32]);
let mut reg = SegmentBorrowRegistry::new();
let first = reg.register_leased_read(&key, 0, 8).unwrap();
let second = reg.register_leased_write(&key, 8, 8).unwrap();
assert!(reg.release(&first));
assert_eq!(reg.len(), 1);
assert!(reg.find_exact(&key, 0, 8, AccessKind::Read).is_none());
assert!(reg.find_exact(&key, 8, 8, AccessKind::Write).is_some());
assert!(reg.release(&second));
assert!(reg.is_empty());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Address;
fn test_addr(seed: u8) -> Address {
Address::new([seed; 32])
}
#[test]
fn read_read_same_range_allowed() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
assert!(reg.register_read(&key, 0, 8).is_ok());
assert!(reg.register_read(&key, 0, 8).is_ok());
assert_eq!(reg.len(), 2);
}
#[test]
fn read_write_same_range_rejected() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
assert!(reg.register_read(&key, 0, 8).is_ok());
assert!(reg.register_write(&key, 0, 8).is_err());
}
#[test]
fn write_write_same_range_rejected() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
assert!(reg.register_write(&key, 0, 8).is_ok());
assert!(reg.register_write(&key, 0, 8).is_err());
}
#[test]
fn write_read_same_range_rejected() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
assert!(reg.register_write(&key, 0, 8).is_ok());
assert!(reg.register_read(&key, 0, 8).is_err());
}
#[test]
fn non_overlapping_write_write_allowed() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
assert!(reg.register_write(&key, 0, 8).is_ok());
assert!(reg.register_write(&key, 8, 32).is_ok());
}
#[test]
fn partially_overlapping_rejected() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
assert!(reg.register_write(&key, 0, 16).is_ok());
assert!(reg.register_write(&key, 8, 16).is_err());
}
#[test]
fn different_accounts_always_allowed() {
let mut reg = SegmentBorrowRegistry::new();
assert!(reg.register_write(&test_addr(1), 0, 8).is_ok());
assert!(reg.register_write(&test_addr(2), 0, 8).is_ok());
}
#[test]
fn release_then_reacquire() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
let borrow = SegmentBorrow {
key_fp: address_fingerprint(&key),
key,
offset: 0,
size: 8,
kind: AccessKind::Write,
};
assert!(reg.register(borrow).is_ok());
assert!(reg.register_write(&key, 0, 8).is_err()); assert!(reg.release(&borrow));
assert!(reg.register_write(&key, 0, 8).is_ok()); }
#[test]
fn release_last_registered_falls_back_to_exact_release() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
let first = reg.register_leased_read(&key, 0, 8).unwrap();
let second = reg.register_leased_write(&key, 8, 8).unwrap();
assert!(unsafe { reg.release_last_registered(&first) });
assert_eq!(reg.len(), 1);
assert!(reg.find_exact(&key, 0, 8, AccessKind::Read).is_none());
assert!(reg.find_exact(&key, 8, 8, AccessKind::Write).is_some());
assert!(unsafe { reg.release_last_registered(&second) });
assert!(reg.is_empty());
}
#[test]
fn capacity_limit() {
let mut reg = SegmentBorrowRegistry::new();
for i in 0..MAX_SEGMENT_BORROWS {
assert!(reg.register_read(&test_addr(1), i as u32 * 8, 8).is_ok());
}
assert!(reg.register_read(&test_addr(1), 256, 8).is_err());
}
#[test]
fn would_conflict_does_not_mutate() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
assert!(reg.register_write(&key, 0, 8).is_ok());
let proposed = SegmentBorrow {
key_fp: address_fingerprint(&key),
key,
offset: 0,
size: 8,
kind: AccessKind::Write,
};
assert!(reg.would_conflict(&proposed));
assert_eq!(reg.len(), 1); }
#[test]
fn adjacent_ranges_no_conflict() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
assert!(reg.register_write(&key, 0, 8).is_ok());
assert!(reg.register_write(&key, 8, 8).is_ok());
}
#[test]
fn guard_auto_releases_write_on_drop() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
{
let _guard = reg.register_guard_write(&key, 0, 8).unwrap();
}
assert_eq!(reg.len(), 0);
assert!(reg.register_write(&key, 0, 8).is_ok());
}
#[test]
fn guard_auto_releases_read_on_drop() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
{
let _guard = reg.register_guard_read(&key, 0, 8).unwrap();
}
assert_eq!(reg.len(), 0);
assert!(reg.register_write(&key, 0, 8).is_ok());
}
#[test]
fn sequential_guards_reuse_slot() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
for _ in 0..4 {
let _guard = reg.register_guard_write(&key, 0, 8).unwrap();
}
assert_eq!(reg.len(), 0);
}
#[test]
fn guard_accessors() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
let guard = reg.register_guard_write(&key, 16, 32).unwrap();
assert_eq!(guard.kind(), AccessKind::Write);
assert_eq!(guard.offset(), 16);
assert_eq!(guard.size(), 32);
}
#[test]
fn guard_then_manual_register_ok() {
let mut reg = SegmentBorrowRegistry::new();
let key = test_addr(1);
{
let _guard = reg.register_guard_write(&key, 0, 8).unwrap();
}
assert!(reg.register_read(&key, 0, 8).is_ok());
assert_eq!(reg.len(), 1);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use crate::Address;
use proptest::prelude::*;
#[derive(Debug, Clone, Copy)]
struct Seg {
offset: u32,
size: u32,
}
fn oracle_overlap(a: Seg, b: Seg) -> bool {
let a_end = a.offset as u64 + a.size as u64;
let b_end = b.offset as u64 + b.size as u64;
(a.offset as u64) < b_end && (b.offset as u64) < a_end
}
fn seg_strategy() -> impl Strategy<Value = Seg> {
(0u32..64, 1u32..16).prop_map(|(offset, size)| Seg { offset, size })
}
proptest! {
#[test]
fn write_borrows_match_disjointness_oracle(
segs in proptest::collection::vec(seg_strategy(), 0..MAX_SEGMENT_BORROWS)
) {
let key = Address::new([42u8; 32]);
let mut reg = SegmentBorrowRegistry::new();
let mut accepted: alloc_vec::Vec<Seg> = alloc_vec::Vec::new();
for seg in segs {
let conflicts = accepted.iter().any(|prev| oracle_overlap(*prev, seg));
let result = reg.register_write(&key, seg.offset, seg.size);
if conflicts {
prop_assert!(
result.is_err(),
"registry accepted an overlapping write {:?} against {:?}",
seg,
accepted
);
} else {
prop_assert!(
result.is_ok(),
"registry rejected a disjoint write {:?} against {:?}",
seg,
accepted
);
accepted.push(seg);
}
}
prop_assert_eq!(reg.len(), accepted.len());
}
#[test]
fn read_borrows_never_conflict(
segs in proptest::collection::vec(seg_strategy(), 0..MAX_SEGMENT_BORROWS)
) {
let key = Address::new([7u8; 32]);
let mut reg = SegmentBorrowRegistry::new();
let n = segs.len();
for seg in segs {
prop_assert!(reg.register_read(&key, seg.offset, seg.size).is_ok());
}
prop_assert_eq!(reg.len(), n);
}
#[test]
fn distinct_accounts_never_conflict(seg in seg_strategy()) {
let mut reg = SegmentBorrowRegistry::new();
prop_assert!(reg.register_write(&Address::new([1u8; 32]), seg.offset, seg.size).is_ok());
prop_assert!(reg.register_write(&Address::new([2u8; 32]), seg.offset, seg.size).is_ok());
prop_assert_eq!(reg.len(), 2);
}
}
mod alloc_vec {
pub use std::vec::Vec;
}
}
#[cfg(all(test, feature = "touch-map"))]
pub(crate) fn decode_touch_map_for_tests(
bytes: &[u8],
) -> Option<(u8, std::vec::Vec<TouchMapRecord>)> {
if bytes.len() < TOUCH_MAP_HEADER_LEN {
return None;
}
if bytes[0] != TOUCH_MAP_MAGIC || bytes[1] != TOUCH_MAP_VERSION {
return None;
}
let flags = bytes[2];
let count = bytes[3] as usize;
if bytes.len() != TOUCH_MAP_HEADER_LEN + count * TOUCH_MAP_RECORD_LEN {
return None;
}
let mut records = std::vec::Vec::with_capacity(count);
for i in 0..count {
let base = TOUCH_MAP_HEADER_LEN + i * TOUCH_MAP_RECORD_LEN;
let packed = u32::from_le_bytes(bytes[base + 1..base + 5].try_into().unwrap());
records.push(TouchMapRecord {
slot: bytes[base],
offset: packed & 0x7FFF_FFFF,
size: u32::from_le_bytes(bytes[base + 5..base + 9].try_into().unwrap()),
write: packed & 0x8000_0000 != 0,
});
}
Some((flags, records))
}
#[cfg(all(test, feature = "touch-map"))]
mod touch_map_tests {
use super::*;
fn key(byte: u8) -> Address {
Address::new([byte; 32])
}
#[test]
fn touch_log_survives_release_and_dedups() {
let mut reg = SegmentBorrowRegistry::new();
let a = reg.register_leased_write(&key(1), 0, 8).unwrap();
let b = reg.register_leased_read(&key(1), 8, 8).unwrap();
let c = reg.register_leased_read(&key(2), 0, 4).unwrap();
assert!(reg.release(&a));
assert!(reg.release(&b));
assert!(reg.release(&c));
assert!(reg.is_empty());
assert_eq!(reg.touch_map_len(), 3);
assert!(!reg.touch_map_overflowed());
let a2 = reg.register_leased_write(&key(1), 0, 8).unwrap();
assert_eq!(reg.touch_map_len(), 3);
reg.release(&a2);
let _a3 = reg.register_leased_read(&key(1), 0, 8).unwrap();
assert_eq!(reg.touch_map_len(), 4);
let mut seen = std::vec::Vec::new();
reg.for_each_touch(|t| seen.push((t.key, t.offset, t.size, t.kind)));
assert_eq!(seen[0], (key(1), 0, 8, AccessKind::Write));
assert_eq!(seen[1], (key(1), 8, 8, AccessKind::Read));
assert_eq!(seen[2], (key(2), 0, 4, AccessKind::Read));
assert_eq!(seen[3], (key(1), 0, 8, AccessKind::Read));
}
#[test]
fn whole_account_touch_recorded_without_live_ledger_entry() {
let mut reg = SegmentBorrowRegistry::new();
let seg = reg.register_leased_write(&key(3), 16, 8).unwrap();
reg.release(&seg);
reg.record_account_touch(&key(3), 64, AccessKind::Write);
assert!(reg.is_empty());
assert!(reg.register_write(&key(3), 0, 8).is_ok());
reg.record_account_touch(&key(3), 64, AccessKind::Write);
assert_eq!(reg.touch_map_len(), 3);
let mut seen = std::vec::Vec::new();
reg.for_each_touch(|t| seen.push((t.offset, t.size, t.kind)));
assert_eq!(seen[0], (16, 8, AccessKind::Write));
assert_eq!(seen[1], (0, 64, AccessKind::Write));
assert_eq!(seen[2], (0, 8, AccessKind::Write));
}
#[test]
fn touch_log_flags_overflow_and_stays_partial_not_wrong() {
let mut reg = SegmentBorrowRegistry::new();
let mut i: u32 = 0;
while (i as usize) < MAX_TOUCH_RECORDS + 3 {
let b = reg.register_leased_read(&key(9), i * 16, 8).unwrap();
reg.release(&b);
i += 1;
}
assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
assert!(reg.touch_map_overflowed());
}
#[test]
fn columnar_contiguous_writes_coalesce_instead_of_overflowing() {
let mut reg = SegmentBorrowRegistry::new();
let cells = MAX_TOUCH_RECORDS * 4;
let mut i: u32 = 0;
while (i as usize) < cells {
let b = reg.register_leased_write(&key(7), i * 8, 8).unwrap();
reg.release(&b);
i += 1;
}
assert!(!reg.touch_map_overflowed());
assert!(reg.touch_map_len() <= MAX_TOUCH_RECORDS);
let total = cells as u64 * 8;
let mut ranges = std::vec::Vec::new();
reg.for_each_touch(|t| {
assert_eq!(t.kind, AccessKind::Write);
assert_eq!(t.key, key(7));
let end = t.offset as u64 + t.size as u64;
assert!(end <= total, "coalesced record claims untouched bytes");
ranges.push((t.offset as u64, end));
});
ranges.sort_unstable();
let mut covered_to = 0u64;
for (start, end) in ranges {
assert!(
start <= covered_to,
"gap in coalesced coverage at {covered_to}"
);
if end > covered_to {
covered_to = end;
}
}
assert_eq!(covered_to, total);
}
#[test]
fn pressure_absorbs_contained_reads_but_never_widens_a_write() {
let mut reg = SegmentBorrowRegistry::new();
let w = reg.register_leased_write(&key(1), 0, 64).unwrap();
reg.release(&w);
let mut i: u32 = 0;
while (i as usize) < MAX_TOUCH_RECORDS - 1 {
let b = reg.register_leased_read(&key(2), i * 16, 8).unwrap();
reg.release(&b);
i += 1;
}
assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
assert!(!reg.touch_map_overflowed());
let r = reg.register_leased_read(&key(1), 4, 4).unwrap();
reg.release(&r);
assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
assert!(!reg.touch_map_overflowed());
let r2 = reg.register_leased_read(&key(1), 60, 8).unwrap();
reg.release(&r2);
assert!(reg.touch_map_overflowed());
reg.for_each_touch(|t| {
if t.kind == AccessKind::Write {
assert_eq!((t.key, t.offset, t.size), (key(1), 0, 64));
}
});
}
#[test]
fn pressure_upgrades_contained_read_to_the_covering_write() {
let mut reg = SegmentBorrowRegistry::new();
let r = reg.register_leased_read(&key(1), 4, 4).unwrap();
reg.release(&r);
let mut i: u32 = 0;
while (i as usize) < MAX_TOUCH_RECORDS - 1 {
let b = reg.register_leased_read(&key(2), i * 16, 8).unwrap();
reg.release(&b);
i += 1;
}
assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
let w = reg.register_leased_write(&key(1), 0, 64).unwrap();
reg.release(&w);
assert!(!reg.touch_map_overflowed());
assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
let mut key1_records = std::vec::Vec::new();
reg.for_each_touch(|t| {
if t.key == key(1) {
key1_records.push((t.offset, t.size, t.kind));
}
});
assert_eq!(key1_records, [(0, 64, AccessKind::Write)]);
}
#[test]
fn pressure_compaction_reclaims_slots_from_mergeable_neighbors() {
let mut reg = SegmentBorrowRegistry::new();
let mut i: u32 = 0;
while (i as usize) < MAX_TOUCH_RECORDS {
let b = reg.register_leased_read(&key(5), i * 8, 8).unwrap();
reg.release(&b);
i += 1;
}
assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
assert!(!reg.touch_map_overflowed());
let b = reg.register_leased_write(&key(6), 0, 8).unwrap();
reg.release(&b);
assert!(!reg.touch_map_overflowed());
assert_eq!(reg.touch_map_len(), 2);
let mut seen = std::vec::Vec::new();
reg.for_each_touch(|t| seen.push((t.key, t.offset, t.size, t.kind)));
assert_eq!(
seen[0],
(key(5), 0, MAX_TOUCH_RECORDS as u32 * 8, AccessKind::Read)
);
assert_eq!(seen[1], (key(6), 0, 8, AccessKind::Write));
}
#[test]
fn merge_exact_rules_are_exact_union_only() {
use super::touch_log::merge_exact;
let mk = |offset: u32, size: u32, kind: AccessKind| SegmentBorrow {
key_fp: address_fingerprint(&key(1)),
key: key(1),
offset,
size,
kind,
};
let m = merge_exact(&mk(0, 8, AccessKind::Write), &mk(8, 8, AccessKind::Write)).unwrap();
assert_eq!((m.offset, m.size, m.kind), (0, 16, AccessKind::Write));
let m = merge_exact(&mk(4, 8, AccessKind::Read), &mk(0, 6, AccessKind::Read)).unwrap();
assert_eq!((m.offset, m.size, m.kind), (0, 12, AccessKind::Read));
assert!(merge_exact(&mk(0, 8, AccessKind::Write), &mk(9, 8, AccessKind::Write)).is_none());
let other = SegmentBorrow {
key_fp: address_fingerprint(&key(2)),
key: key(2),
offset: 8,
size: 8,
kind: AccessKind::Write,
};
assert!(merge_exact(&mk(0, 8, AccessKind::Write), &other).is_none());
let m = merge_exact(&mk(4, 4, AccessKind::Read), &mk(0, 64, AccessKind::Write)).unwrap();
assert_eq!((m.offset, m.size, m.kind), (0, 64, AccessKind::Write));
let m = merge_exact(&mk(0, 64, AccessKind::Write), &mk(4, 4, AccessKind::Read)).unwrap();
assert_eq!((m.offset, m.size, m.kind), (0, 64, AccessKind::Write));
assert!(merge_exact(&mk(60, 8, AccessKind::Read), &mk(0, 64, AccessKind::Write)).is_none());
assert!(merge_exact(&mk(0, 64, AccessKind::Write), &mk(60, 8, AccessKind::Read)).is_none());
assert!(merge_exact(
&mk(0, u32::MAX, AccessKind::Write),
&mk(u32::MAX - 1, 2, AccessKind::Write),
)
.is_none());
}
#[test]
fn touch_map_encoder_round_trips_including_flags() {
let records = [
TouchMapRecord {
slot: 0,
offset: 16,
size: 8,
write: true,
},
TouchMapRecord {
slot: 3,
offset: 0,
size: 64,
write: false,
},
TouchMapRecord {
slot: 255,
offset: 0x7FFF_FFFF,
size: 1,
write: true,
},
];
let (buf, len) = encode_touch_map(&records, false, false);
assert_eq!(len, TOUCH_MAP_HEADER_LEN + 3 * TOUCH_MAP_RECORD_LEN);
let (flags, decoded) = decode_touch_map_for_tests(&buf[..len]).unwrap();
assert_eq!(flags, 0);
assert_eq!(decoded, records);
let (buf, len) = encode_touch_map(&records, true, false);
let (flags, decoded) = decode_touch_map_for_tests(&buf[..len]).unwrap();
assert_eq!(flags, TOUCH_MAP_FLAG_OVERFLOWED);
assert_eq!(decoded, records);
let (buf, len) = encode_touch_map(&records, false, true);
let (flags, _) = decode_touch_map_for_tests(&buf[..len]).unwrap();
assert_eq!(flags, TOUCH_MAP_FLAG_SKIPPED);
}
#[test]
fn touch_map_encoder_skips_unrepresentable_offsets_honestly() {
let records = [
TouchMapRecord {
slot: 0,
offset: 8,
size: 8,
write: false,
},
TouchMapRecord {
slot: 1,
offset: 0x8000_0000, size: 8,
write: true,
},
];
let (buf, len) = encode_touch_map(&records, false, false);
let (flags, decoded) = decode_touch_map_for_tests(&buf[..len]).unwrap();
assert_eq!(flags, TOUCH_MAP_FLAG_SKIPPED);
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0], records[0]);
}
#[test]
fn touch_map_encoder_never_lies_about_record_count() {
let records = std::vec![
TouchMapRecord {
slot: 0,
offset: 0,
size: 1,
write: false,
};
MAX_TOUCH_RECORDS + 2
];
let (buf, len) = encode_touch_map(&records, false, false);
assert_eq!(
len,
TOUCH_MAP_HEADER_LEN + MAX_TOUCH_RECORDS * TOUCH_MAP_RECORD_LEN
);
let (flags, decoded) = decode_touch_map_for_tests(&buf[..len]).unwrap();
assert_eq!(flags & TOUCH_MAP_FLAG_OVERFLOWED, TOUCH_MAP_FLAG_OVERFLOWED);
assert_eq!(decoded.len(), MAX_TOUCH_RECORDS);
}
#[test]
fn touch_map_decoder_rejects_wrong_magic_version_and_length() {
let (buf, len) = encode_touch_map(
&[TouchMapRecord {
slot: 0,
offset: 4,
size: 4,
write: true,
}],
false,
false,
);
assert!(decode_touch_map_for_tests(&buf[..len]).is_some());
let mut bad_magic = buf;
bad_magic[0] = 0x7B;
assert!(decode_touch_map_for_tests(&bad_magic[..len]).is_none());
let mut bad_version = buf;
bad_version[1] = 0x02;
assert!(decode_touch_map_for_tests(&bad_version[..len]).is_none());
assert!(decode_touch_map_for_tests(&buf[..len - 1]).is_none());
assert!(decode_touch_map_for_tests(&buf[..len + 9]).is_none());
}
#[cfg(feature = "touch-map")]
#[test]
fn initial_touch_log_is_all_zero_bytes() {
let zeroed = std::vec![0u64; core::mem::size_of::<touch_log::TouchLog>().div_ceil(8)];
touch_log::assert_all_zero_is_the_valid_empty_log(&zeroed);
}
#[cfg(feature = "touch-map")]
#[test]
fn bare_account_view_load_mut_records_ambiently() {
use crate::layout::{write_header, HopperHeader, LayoutContract};
use hopper_native::{
AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
NOT_BORROWED,
};
#[repr(C)]
#[derive(Clone, Copy)]
struct Blob {
v: [u8; 8],
}
unsafe impl crate::Zeroable for Blob {}
unsafe impl crate::Pod for Blob {}
unsafe impl crate::zerocopy::__sealed::HopperZeroCopySealed for Blob {}
impl crate::field_map::FieldMap for Blob {
const FIELDS: &'static [crate::field_map::FieldInfo] =
&[crate::field_map::FieldInfo::new("v", HopperHeader::SIZE, 8)];
}
impl LayoutContract for Blob {
const DISC: u8 = 55;
const VERSION: u8 = 1;
const LAYOUT_ID: [u8; 8] = [0x55; 8];
const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
}
const DATA_LEN: usize = HopperHeader::SIZE + 8;
let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + DATA_LEN).div_ceil(8)];
let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
unsafe {
raw.write(RuntimeAccount {
borrow_state: NOT_BORROWED,
is_signer: 0,
is_writable: 1,
executable: 0,
resize_delta: 0,
address: NativeAddress::new_from_array([3; 32]),
owner: NativeAddress::new_from_array([4; 32]),
lamports: 1,
data_len: DATA_LEN as u64,
});
}
let backend = unsafe { NativeAccountView::new_unchecked(raw) };
let account = crate::AccountView::from_backend(backend);
{
let mut data = account.try_borrow_mut().unwrap();
write_header(
&mut data,
<Blob as LayoutContract>::DISC,
<Blob as LayoutContract>::VERSION,
&<Blob as LayoutContract>::LAYOUT_ID,
)
.unwrap();
}
touch_log::reset();
assert_eq!(touch_log::len(), 0, "raw try_borrow_mut must not record");
drop(account.load_mut::<Blob>().unwrap());
let mut seen = std::vec::Vec::new();
touch_log::for_each(|t| seen.push((t.key, t.offset, t.size, t.kind)));
assert_eq!(
seen,
std::vec![(*account.address(), 0, DATA_LEN as u32, AccessKind::Write)],
"a Context-less typed load_mut must land in the ambient log"
);
let pid = crate::address::Address::new([9u8; 32]);
let accounts: [crate::AccountView<'_>; 0] = [];
let _ctx = crate::context::Context::new(&pid, &accounts, &[]);
assert_eq!(
touch_log::len(),
0,
"Context::new must reset the ambient log"
);
}
}