hopper_runtime/segment_borrow.rs
1//! Segment-level borrow registry for fine-grained access control.
2//!
3//! The account-level borrow registry prevents
4//! aliasing across entire accounts. This module adds **segment-level**
5//! conflict detection: two borrows of the *same* account are allowed when
6//! their byte ranges don't overlap, or when both are read-only.
7//!
8//! ## Conflict Rules
9//!
10//! | Existing | New | Overlapping? | Allowed |
11//! |----------|-------|--------------|---------|
12//! | Read | Read | yes | ✅ |
13//! | Read | Write | yes | ❌ |
14//! | Write | Read | yes | ❌ |
15//! | Write | Write | yes | ❌ |
16//! | *any* | *any* | no | ✅ |
17//!
18//! ## Bounded representation
19//!
20//! - Fixed-capacity array (no heap)
21//! - Inline conflict checks
22//! - Deterministic iteration (bounded loop)
23
24use core::mem::MaybeUninit;
25
26use crate::address::Address;
27use crate::error::ProgramError;
28
29/// Maximum simultaneous segment borrows per instruction.
30///
31/// 16 covers any realistic instruction, most use 2-6 segments.
32/// Keeping it fixed avoids heap allocation while staying well within
33/// Solana's CU budget. The compact entry representation keeps the
34/// total stack footprint under 200 bytes.
35pub const MAX_SEGMENT_BORROWS: usize = 16;
36
37/// Read or write access intent for a segment borrow.
38#[derive(Clone, Copy, PartialEq, Eq, Debug)]
39#[repr(u8)]
40pub enum AccessKind {
41 /// Shared (immutable) access.
42 Read = 0,
43 /// Exclusive (mutable) access.
44 Write = 1,
45}
46
47/// First-8-byte prefix of an account address, used as a fast-path
48/// comparator in the conflict scan.
49///
50/// The **audit-correct** model is fingerprint-then-verify: a hot-path
51/// `u64` compare rejects unrelated accounts immediately; the slow-path
52/// 32-byte compare fires only when the prefixes match. Because a
53/// full-address compare always follows, fingerprint collisions produce
54/// **no** false conflicts, they only cost one extra 32-byte compare
55/// for the extremely rare collision pair.
56#[inline(always)]
57fn address_fingerprint(address: &Address) -> u64 {
58 let bytes = address.as_array();
59 u64::from_le_bytes([
60 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
61 ])
62}
63
64/// Full-identity equality check on the slow path.
65#[inline(always)]
66fn address_eq(a: &Address, b: &Address) -> bool {
67 a.as_array() == b.as_array()
68}
69
70#[inline(always)]
71fn borrow_eq(a: &SegmentBorrow, b: &SegmentBorrow) -> bool {
72 a.key_fp == b.key_fp
73 && address_eq(&a.key, &b.key)
74 && a.offset == b.offset
75 && a.size == b.size
76 && a.kind == b.kind
77}
78
79/// A single active segment borrow.
80///
81/// Carries both a fast `u64` fingerprint and the full 32-byte account
82/// address. The fingerprint is the hot-path comparator; the full
83/// address resolves collisions so conflict detection is never
84/// probabilistic.
85#[derive(Clone, Copy, Debug)]
86pub struct SegmentBorrow {
87 /// Fast-path prefix of the account address.
88 pub key_fp: u64,
89 /// Full account address, authoritative identity, checked whenever
90 /// the fast-path fingerprint matches. Previously the implementation relied on the
91 /// fingerprint alone and claimed it was "collision-free for any
92 /// realistic instruction"; that was probabilistic, not a guarantee.
93 pub key: Address,
94 /// Byte offset within the account data.
95 pub offset: u32,
96 /// Byte size of the borrowed segment.
97 pub size: u32,
98 /// Access kind (read or write).
99 pub kind: AccessKind,
100}
101
102/// Check whether two byte ranges overlap.
103#[inline(always)]
104const fn ranges_overlap(a_off: u32, a_size: u32, b_off: u32, b_size: u32) -> bool {
105 let a_end = a_off as u64 + a_size as u64;
106 let b_end = b_off as u64 + b_size as u64;
107 // Non-overlapping iff one ends before the other starts.
108 !(a_end <= b_off as u64 || b_end <= a_off as u64)
109}
110
111/// Instruction-scoped segment borrow registry.
112///
113/// Tracks active segment borrows and enforces conflict rules. Designed
114/// for inline use in an execution context, no heap, no dynamic dispatch.
115///
116/// Uses compact 8-byte address fingerprints and a flat array of
117/// fixed-size entries. Total stack footprint: ~280 bytes (vs ~1.3 KB
118/// with full 32-byte addresses and Option wrappers).
119///
120/// # Example
121///
122/// ```ignore
123/// let mut borrows = SegmentBorrowRegistry::new();
124/// borrows.register_read(&vault_key, 0, 8)?; // read balance
125/// borrows.register_write(&vault_key, 8, 32)?; // write metadata, OK, non-overlapping
126/// borrows.register_write(&vault_key, 0, 8)?; // REJECTED, overlaps read
127/// ```
128pub struct SegmentBorrowRegistry {
129 entries: [MaybeUninit<SegmentBorrow>; MAX_SEGMENT_BORROWS],
130 len: u8,
131 // The touch LOG does not live here. It records every distinct
132 // `(account, offset, size, kind)` the instruction ever touched,
133 // including whole-account borrows taken straight off an
134 // `AccountView` with no `Context` (wrapper `get_mut`, raw
135 // `load_mut`); so its storage is instruction-ambient (see
136 // [`touch_log`]), the same three-tier scheme as the lamport gate
137 // store. The registry keeps the recording/introspection API and
138 // delegates.
139}
140
141/// Capacity of the instruction touch log (`touch-map` feature).
142///
143/// This caps *slots*, not coverage: at capacity the log coalesces
144/// records whose union is exactly the touched byte set (see
145/// [`touch_log`]), so contiguous same-kind workloads of any size, columnar
146/// writes, sequence pushes, still produce a COMPLETE map. Overflow (a
147/// partial map, flagged on the wire) now requires more than this many
148/// *pairwise-unmergeable* ranges in one instruction.
149#[cfg(feature = "touch-map")]
150pub const MAX_TOUCH_RECORDS: usize = 32;
151
152// ---------------------------------------------------------------------------
153// Touch-map wire format v1 (`touch-map` feature)
154// ---------------------------------------------------------------------------
155//
156// A touch map is emitted as ONE `sol_log_data` segment (it appears in the
157// transaction log as a `Program data: <base64>` line) and is a **public,
158// versioned wire format**, decoders exist in `hopper tx explain`, in the
159// generated TypeScript client (`decodeHopperTouchMap`), and in this module's
160// tests. Any change to the layout below requires bumping
161// [`TOUCH_MAP_VERSION`].
162//
163// ```text
164// byte 0 magic = 0x7A (TOUCH_MAP_MAGIC)
165// byte 1 version = 0x01 (TOUCH_MAP_VERSION)
166// byte 2 flags bit0 = touch log overflowed (map is partial)
167// bit1 = one or more records were skipped by
168// the encoder (unmappable address or
169// offset >= 2^31)
170// bits 2-7 reserved, zero in v1
171// byte 3 count number of records that follow (0..=32)
172// bytes 4..4+9n records 9 bytes each:
173// +0 slot u8 account index into the instruction's
174// account list
175// +1..+5 packed u32 LE; top bit = write (1) / read (0),
176// low 31 bits = byte offset in account data
177// +5..+9 size u32 LE byte length of the touched range
178// ```
179//
180// Total length is always exactly `4 + 9 * count` (<= 292 bytes). Decoders
181// MUST verify magic, version, and the exact-length equation; together these
182// make accidental collision with other `Program data:` payloads (e.g.
183// Anchor's 8-byte sha256 event discriminators, whose first byte is
184// uniformly distributed) practically impossible, a colliding payload would
185// need byte0 = 0x7A, byte1 = 0x01, and a total length satisfying the count
186// equation.
187//
188// Honesty rules: a map with flag bit0 set is PARTIAL, the instruction
189// touched more than [`MAX_TOUCH_RECORDS`] pairwise-unmergeable ranges (the
190// log coalesces exact unions under pressure before ever declaring a map
191// partial; see [`touch_log`]); a map with flag bit1 set omitted at least
192// one touched range it could not represent. Consumers must not treat such
193// maps as a complete effect set.
194
195/// Magic byte identifying a touch-map `sol_log_data` record ('z').
196#[cfg(feature = "touch-map")]
197pub const TOUCH_MAP_MAGIC: u8 = 0x7A;
198/// Touch-map wire format version.
199#[cfg(feature = "touch-map")]
200pub const TOUCH_MAP_VERSION: u8 = 0x01;
201/// Flags bit0: the touch log overflowed; the map is partial.
202#[cfg(feature = "touch-map")]
203pub const TOUCH_MAP_FLAG_OVERFLOWED: u8 = 1 << 0;
204/// Flags bit1: the encoder skipped at least one record (address not among
205/// the instruction accounts, slot index above `u8::MAX`, or offset not
206/// representable in 31 bits).
207#[cfg(feature = "touch-map")]
208pub const TOUCH_MAP_FLAG_SKIPPED: u8 = 1 << 1;
209/// Fixed header length (magic, version, flags, count).
210#[cfg(feature = "touch-map")]
211pub const TOUCH_MAP_HEADER_LEN: usize = 4;
212/// Encoded length of one touch record.
213#[cfg(feature = "touch-map")]
214pub const TOUCH_MAP_RECORD_LEN: usize = 9;
215/// Maximum encoded touch-map length (292 bytes).
216#[cfg(feature = "touch-map")]
217pub const TOUCH_MAP_MAX_ENCODED_LEN: usize =
218 TOUCH_MAP_HEADER_LEN + MAX_TOUCH_RECORDS * TOUCH_MAP_RECORD_LEN;
219
220/// One slot-resolved touch record, ready for wire encoding
221/// (`touch-map` feature). Unlike [`SegmentBorrow`], the account is
222/// identified by its index in the instruction's account list, which is
223/// what an off-chain decoder can join against a fetched transaction.
224#[cfg(feature = "touch-map")]
225#[derive(Clone, Copy, Debug, PartialEq, Eq)]
226pub struct TouchMapRecord {
227 /// Account slot index into the instruction's account list.
228 pub slot: u8,
229 /// Byte offset within the account data (must fit in 31 bits).
230 pub offset: u32,
231 /// Byte length of the touched range.
232 pub size: u32,
233 /// Write access (`true`) or read access (`false`).
234 pub write: bool,
235}
236
237/// Encode a touch map into the versioned v1 wire format documented above
238/// (`touch-map` feature). Pure function: no syscalls, no allocation,
239/// returns the fixed-capacity buffer plus the number of valid bytes.
240///
241/// `overflowed` must be the touch log's overflow flag so partial maps are
242/// honestly marked. `skipped` must be `true` when the caller dropped any
243/// record while resolving addresses to slots. Records whose `offset` does
244/// not fit in 31 bits are skipped here (impossible for real Solana
245/// accounts, which cap at 10 MiB) and reported via flag bit1 rather than
246/// silently truncated. If more than [`MAX_TOUCH_RECORDS`] records are
247/// passed, the excess is dropped and the overflow flag is set, the
248/// record count byte never lies about the encoded payload.
249#[cfg(feature = "touch-map")]
250pub fn encode_touch_map(
251 records: &[TouchMapRecord],
252 overflowed: bool,
253 skipped: bool,
254) -> ([u8; TOUCH_MAP_MAX_ENCODED_LEN], usize) {
255 let mut buf = [0u8; TOUCH_MAP_MAX_ENCODED_LEN];
256 let mut flags = 0u8;
257 if overflowed {
258 flags |= TOUCH_MAP_FLAG_OVERFLOWED;
259 }
260 if skipped {
261 flags |= TOUCH_MAP_FLAG_SKIPPED;
262 }
263 let mut count = 0usize;
264 let mut pos = TOUCH_MAP_HEADER_LEN;
265 for rec in records {
266 if count >= MAX_TOUCH_RECORDS {
267 flags |= TOUCH_MAP_FLAG_OVERFLOWED;
268 break;
269 }
270 if rec.offset > i32::MAX as u32 {
271 flags |= TOUCH_MAP_FLAG_SKIPPED;
272 continue;
273 }
274 let packed = rec.offset | if rec.write { 0x8000_0000 } else { 0 };
275 buf[pos] = rec.slot;
276 buf[pos + 1..pos + 5].copy_from_slice(&packed.to_le_bytes());
277 buf[pos + 5..pos + 9].copy_from_slice(&rec.size.to_le_bytes());
278 pos += TOUCH_MAP_RECORD_LEN;
279 count += 1;
280 }
281 buf[0] = TOUCH_MAP_MAGIC;
282 buf[1] = TOUCH_MAP_VERSION;
283 buf[2] = flags;
284 buf[3] = count as u8;
285 (buf, pos)
286}
287
288/// Instruction-ambient touch log (`touch-map` feature).
289///
290/// The log used to live inside [`SegmentBorrowRegistry`]; which meant
291/// only `Context`-mediated borrows could record, and whole-account
292/// borrows taken straight off an `AccountView` (wrapper `get_mut`, raw
293/// `load_mut`) were a disclosed blind spot in the emitted touch map.
294/// Moving the storage to the same instruction-ambient scheme as the
295/// lamport gate store closes that: `AccountView::try_borrow_mut`
296/// records its own footprint with no `Context` in reach, so EVERY
297/// mutable data borrow, segment lease, typed load, wrapper accessor,
298/// lifecycle write, lands in the same log.
299///
300/// Storage tiers mirror `write_policy::gate_store` exactly:
301///
302/// - **SBF**: the reserved bottom of the VM heap, right after the gate
303/// store. Deployed programs cannot carry writable sections at all
304/// (the loader rejects `.bss`/`.data`), and the VM zeroes the heap
305/// on every invocation, and an all-zero [`TouchLog`] IS the valid
306/// empty log (pinned by a test), so instruction scoping is free and
307/// no init code runs. Each CPI level is its own VM with its own
308/// heap, so levels never share a log.
309/// - **Host, `test`/`thread-local-registry`**: per-thread storage, so
310/// parallel test threads never observe each other's logs.
311/// - **Host fallback** (`no_std` hosts without the feature): one
312/// process-global spinlocked log. Cross-thread sharing means
313/// concurrent instructions pollute each other's MAPS (never memory
314/// safety), the same documented imprecision as the fallback borrow
315/// registry. [`Context::new`](crate::context::Context::new) resets
316/// the log, which keeps single-threaded hosts exact.
317///
318/// ## Degradation ladder (honesty under pressure)
319///
320/// Below capacity the log is **granular**: one record per distinct
321/// `(account, offset, size, kind)`, which is what lets `hopper tx
322/// explain` name individual fields. At capacity it **coalesces**:
323/// records whose union is exactly the touched byte set merge
324/// ([`merge_exact`]), granularity degrades, coverage stays exact and
325/// complete, and the map carries no flag because it is not partial.
326/// Only when an incoming range cannot be absorbed AND no pair of
327/// records is mergeable does the log set `overflow`, a PARTIAL map,
328/// flagged as such on the wire. Contiguous large workloads therefore
329/// never produce a partial map; only more than [`MAX_TOUCH_RECORDS`]
330/// pairwise-unmergeable ranges (including alternating read/write ranges) do.
331#[cfg(feature = "touch-map")]
332pub(crate) mod touch_log {
333 use super::{
334 address_eq, address_fingerprint, borrow_eq, AccessKind, SegmentBorrow, MAX_TOUCH_RECORDS,
335 };
336 use crate::address::Address;
337
338 /// Merge two touch records when their union is EXACTLY the byte set
339 /// the pair touched, the rule that lets a full log trade
340 /// granularity for completeness instead of declaring a partial map.
341 ///
342 /// Two records merge only when they name the same account and:
343 ///
344 /// - **same kind, overlapping or adjacent** → the union range. A gap
345 /// never bridges: the union would claim bytes the instruction
346 /// never touched.
347 /// - **a read wholly contained in a write** → the write record,
348 /// unchanged. The write already claims strictly more access than
349 /// the read, so dropping the narrower read loses no coverage. A
350 /// write is NEVER widened by a read, that would claim write
351 /// access to bytes that were only read.
352 ///
353 /// A same-kind union whose size exceeds `u32` is refused (both
354 /// records stay) rather than truncated, unreachable for real
355 /// accounts (10 MiB cap) but the guard keeps the function total.
356 pub(crate) fn merge_exact(a: &SegmentBorrow, b: &SegmentBorrow) -> Option<SegmentBorrow> {
357 if a.key_fp != b.key_fp || !address_eq(&a.key, &b.key) {
358 return None;
359 }
360 let a_end = a.offset as u64 + a.size as u64;
361 let b_end = b.offset as u64 + b.size as u64;
362 if a.kind == b.kind {
363 if b.offset as u64 > a_end || a.offset as u64 > b_end {
364 return None;
365 }
366 let offset = if a.offset < b.offset {
367 a.offset
368 } else {
369 b.offset
370 };
371 let end = if a_end > b_end { a_end } else { b_end };
372 let size = end - offset as u64;
373 if size > u32::MAX as u64 {
374 return None;
375 }
376 let mut merged = *a;
377 merged.offset = offset;
378 merged.size = size as u32;
379 return Some(merged);
380 }
381 // Kinds differ, so exactly one of the pair is the write.
382 let (read, write) = if a.kind == AccessKind::Write {
383 (b, a)
384 } else {
385 (a, b)
386 };
387 let read_end = read.offset as u64 + read.size as u64;
388 let write_end = write.offset as u64 + write.size as u64;
389 if write.offset <= read.offset && read_end <= write_end {
390 return Some(*write);
391 }
392 None
393 }
394
395 /// The ambient log: a deduplicated record array that releases never
396 /// shrink (the instruction's cumulative footprint) and that
397 /// coalesces exact unions under capacity pressure ([`merge_exact`]).
398 ///
399 /// INVARIANT (load-bearing on SBF): the all-zero byte pattern is a
400 /// valid, EMPTY log, `len = 0`, `overflow = 0`, entries ignored.
401 /// The SBF tier materializes this struct over zeroed VM heap with
402 /// no initialization whatsoever.
403 #[repr(C)]
404 pub(crate) struct TouchLog {
405 len: u8,
406 overflow: u8,
407 _pad: [u8; 6],
408 entries: [SegmentBorrow; MAX_TOUCH_RECORDS],
409 }
410
411 impl TouchLog {
412 #[cfg_attr(target_os = "solana", allow(dead_code))]
413 pub(crate) const fn new() -> Self {
414 const ZERO: SegmentBorrow = SegmentBorrow {
415 key_fp: 0,
416 key: Address::new_from_array([0u8; 32]),
417 offset: 0,
418 size: 0,
419 kind: AccessKind::Read,
420 };
421 Self {
422 len: 0,
423 overflow: 0,
424 _pad: [0; 6],
425 entries: [ZERO; MAX_TOUCH_RECORDS],
426 }
427 }
428
429 fn record(&mut self, borrow: &SegmentBorrow) {
430 let len = self.len as usize;
431 let mut i = 0;
432 while i < len {
433 // Slots below `len` were written by this method.
434 if borrow_eq(&self.entries[i], borrow) {
435 return;
436 }
437 i += 1;
438 }
439 if len >= MAX_TOUCH_RECORDS {
440 self.record_under_pressure(borrow);
441 return;
442 }
443 self.entries[len] = *borrow;
444 self.len = (len + 1) as u8;
445 }
446
447 /// Full-log path: degrade GRANULARITY, never coverage.
448 ///
449 /// 1. **Absorb**, merge the incoming range into an existing
450 /// record when the union is exactly the touched byte set
451 /// ([`merge_exact`]). The backward scan hits the hot case,
452 /// a loop extending the most recently recorded range
453 /// (columnar writes, sequence pushes), in one step.
454 /// 2. **Compact**, coalesce the log itself; a freed slot takes
455 /// the incoming record verbatim.
456 /// 3. **Overflow**, only when the instruction has touched more
457 /// than [`MAX_TOUCH_RECORDS`] pairwise-unmergeable ranges is
458 /// the map declared partial.
459 ///
460 /// Once `overflow` is set the log is partial for good, a
461 /// dropped range cannot be un-dropped; so later records still
462 /// absorb (coverage keeps improving for free) but the
463 /// quadratic compaction is not retried.
464 fn record_under_pressure(&mut self, borrow: &SegmentBorrow) {
465 let len = self.len as usize;
466 let mut i = len;
467 while i > 0 {
468 i -= 1;
469 if let Some(merged) = merge_exact(&self.entries[i], borrow) {
470 self.entries[i] = merged;
471 return;
472 }
473 }
474 if self.overflow != 0 {
475 return;
476 }
477 if self.compact() {
478 let len = self.len as usize;
479 self.entries[len] = *borrow;
480 self.len = (len + 1) as u8;
481 return;
482 }
483 self.overflow = 1;
484 }
485
486 /// Coalesce every exact-mergeable pair to a fixpoint, preserving
487 /// first-touch order (a merged record keeps the slot of its
488 /// earliest constituent). Returns whether at least one slot was
489 /// freed. Bounded: each merging pass shrinks the log by at least
490 /// one record, so the outer loop runs at most
491 /// [`MAX_TOUCH_RECORDS`] times.
492 fn compact(&mut self) -> bool {
493 let before = self.len;
494 loop {
495 let mut merged_any = false;
496 let mut i = 0;
497 while i < self.len as usize {
498 let mut j = i + 1;
499 while j < self.len as usize {
500 if let Some(merged) = merge_exact(&self.entries[i], &self.entries[j]) {
501 self.entries[i] = merged;
502 self.remove_at(j);
503 merged_any = true;
504 // The removal shifted the next candidate
505 // into slot `j`, do not advance.
506 } else {
507 j += 1;
508 }
509 }
510 i += 1;
511 }
512 if !merged_any {
513 return self.len < before;
514 }
515 }
516 }
517
518 /// Remove `entries[idx]`, shifting the tail left so first-touch
519 /// order survives (a swap-remove would not preserve it).
520 fn remove_at(&mut self, idx: usize) {
521 let len = self.len as usize;
522 let mut k = idx;
523 while k + 1 < len {
524 self.entries[k] = self.entries[k + 1];
525 k += 1;
526 }
527 self.len = (len - 1) as u8;
528 }
529
530 fn for_each<F: FnMut(&SegmentBorrow)>(&self, mut f: F) {
531 let len = self.len as usize;
532 let mut i = 0;
533 while i < len {
534 f(&self.entries[i]);
535 i += 1;
536 }
537 }
538
539 fn reset(&mut self) {
540 self.len = 0;
541 self.overflow = 0;
542 }
543 }
544
545 /// Test hook for the SBF heap-tier invariant, in BOTH directions
546 /// and without ever reading a padding byte (a byte-view of the
547 /// struct reads uninitialized padding, UB the Miri lane caught in
548 /// the previous form of this pin):
549 ///
550 /// 1. an all-zero, 8-aligned region overlays as a VALID, EMPTY log
551 /// (exactly how the VM heap tier materializes it, no init code);
552 /// 2. `TouchLog::new()`'s initialized fields are field-for-field
553 /// the all-zero pattern, so the host tiers and the heap tier
554 /// start from the same state.
555 #[cfg(test)]
556 pub(crate) fn assert_all_zero_is_the_valid_empty_log(zeroed_backing: &[u64]) {
557 assert!(zeroed_backing.len() * 8 >= core::mem::size_of::<TouchLog>());
558 assert!(zeroed_backing.iter().all(|&w| w == 0));
559 // SAFETY: `zeroed_backing` is 8-aligned (u64 slice), fully
560 // initialized, and at least `size_of::<TouchLog>()` bytes; the
561 // all-zero pattern is exactly the claimed-valid pattern under
562 // test (repr(C), integers + byte arrays + a fieldless enum
563 // whose 0 discriminant is `AccessKind::Read`).
564 let overlaid = unsafe { &*(zeroed_backing.as_ptr() as *const TouchLog) };
565 assert_eq!(overlaid.len, 0, "zeroed heap must read as the empty log");
566 assert_eq!(overlaid.overflow, 0);
567
568 let fresh = TouchLog::new();
569 assert_eq!(fresh.len, 0);
570 assert_eq!(fresh.overflow, 0);
571 assert!(fresh._pad.iter().all(|&b| b == 0));
572 let mut i = 0;
573 while i < MAX_TOUCH_RECORDS {
574 let e = &fresh.entries[i];
575 assert!(e.key_fp == 0 && e.offset == 0 && e.size == 0);
576 assert!(matches!(e.kind, AccessKind::Read), "0 must decode as Read");
577 assert!(e.key.as_array().iter().all(|&b| b == 0));
578 i += 1;
579 }
580 }
581
582 /// Record one touch (deduplicated by exact identity; coalesced by
583 /// exact union once the log is full).
584 #[inline]
585 pub(crate) fn record(borrow: &SegmentBorrow) {
586 with_log(|log| log.record(borrow));
587 }
588
589 /// Record a whole-account footprint as a `(0, data_len)` entry.
590 #[inline]
591 pub(crate) fn record_account(key: &Address, data_len: u32, kind: AccessKind) {
592 let borrow = SegmentBorrow {
593 key_fp: address_fingerprint(key),
594 key: *key,
595 offset: 0,
596 size: data_len,
597 kind,
598 };
599 record(&borrow);
600 }
601
602 /// Visit every recorded range, first-touch order (a record
603 /// coalesced under pressure keeps the slot of its earliest
604 /// constituent).
605 #[inline]
606 pub(crate) fn for_each<F: FnMut(&SegmentBorrow)>(f: F) {
607 with_log(|log| log.for_each(f));
608 }
609
610 /// Number of distinct records captured.
611 #[inline]
612 pub(crate) fn len() -> usize {
613 with_log(|log| log.len as usize)
614 }
615
616 /// Whether the log dropped a range it could neither store, absorb,
617 /// nor make room for by compaction, the map is partial.
618 #[inline]
619 pub(crate) fn overflowed() -> bool {
620 with_log(|log| log.overflow != 0)
621 }
622
623 /// Clear the log, the start-of-instruction reset `Context::new`
624 /// performs. On SBF this is redundant with per-invocation heap
625 /// zeroing (kept because it is two byte-writes and makes the
626 /// contract independent of who created how many contexts); on hosts
627 /// it is what scopes the ambient log to an instruction.
628 #[inline]
629 pub(crate) fn reset() {
630 with_log(|log| log.reset());
631 }
632
633 #[cfg(target_os = "solana")]
634 mod store {
635 use super::TouchLog;
636
637 /// Byte offset of the touch log inside the VM heap region:
638 /// right after the lamport gate store (which itself sits after
639 /// the `BumpAllocator` cursor word), rounded up to 8.
640 const TOUCH_HEAP_OFFSET: usize = (crate::write_policy::SBF_GATE_HEAP_END + 7) & !7;
641
642 // The log must fit the reserved runtime scratch alongside the
643 // gate store, and start 8-aligned (SegmentBorrow leads with a
644 // u64, so TouchLog is 8-aligned).
645 const _: () = assert!(
646 TOUCH_HEAP_OFFSET + core::mem::size_of::<TouchLog>()
647 <= hopper_native::HEAP_RUNTIME_RESERVED,
648 "TouchLog exceeds HEAP_RUNTIME_RESERVED; grow the reservation in \
649 hopper-native/src/entrypoint.rs or shrink MAX_TOUCH_RECORDS"
650 );
651 const _: () = assert!((hopper_native::HEAP_START_ADDRESS + TOUCH_HEAP_OFFSET) % 8 == 0);
652
653 /// Same argument as `write_policy::gate_store::with_store`,
654 /// verbatim: single-threaded SBF, the closures never re-enter
655 /// this module, the VM maps AND ZEROES this heap range per
656 /// invocation, all-zero is a valid empty `TouchLog` (pinned by
657 /// `initial_touch_log_is_all_zero_bytes`), the address is
658 /// 8-aligned and the whole object lies inside
659 /// `HEAP_RUNTIME_RESERVED`, which the `BumpAllocator` floor
660 /// excludes and no other Hopper code touches (the gate store
661 /// ends where this offset begins, const-asserted there).
662 pub(super) fn with_log<R>(f: impl FnOnce(&mut TouchLog) -> R) -> R {
663 let ptr = (hopper_native::HEAP_START_ADDRESS + TOUCH_HEAP_OFFSET) as *mut TouchLog;
664 // SAFETY: see the doc comment above, unique access
665 // (single-threaded, non-reentrant closures), valid pointee
666 // (zeroed per invocation = valid empty log), 8-aligned,
667 // in-bounds of the reserved region (both const-asserted).
668 f(unsafe { &mut *ptr })
669 }
670 }
671
672 #[cfg(all(
673 not(target_os = "solana"),
674 any(test, feature = "thread-local-registry")
675 ))]
676 mod store {
677 use super::TouchLog;
678 use std::cell::RefCell;
679
680 // Per-thread log: this crate's unit tests get it via `test`;
681 // downstream test binaries opt in through the same
682 // `thread-local-registry` feature the borrow registry and gate
683 // store use, so parallel test threads never observe each
684 // other's touches.
685 std::thread_local! {
686 static LOG: RefCell<TouchLog> = const { RefCell::new(TouchLog::new()) };
687 }
688
689 pub(super) fn with_log<R>(f: impl FnOnce(&mut TouchLog) -> R) -> R {
690 LOG.with(|cell| f(&mut cell.borrow_mut()))
691 }
692 }
693
694 #[cfg(all(
695 not(target_os = "solana"),
696 not(any(test, feature = "thread-local-registry"))
697 ))]
698 mod store {
699 use super::TouchLog;
700 use core::cell::UnsafeCell;
701 use core::sync::atomic::{AtomicBool, Ordering};
702
703 /// Host fallback tier (`no_std` hosts without the thread-local
704 /// feature): one process-global spinlocked log, the same shape
705 /// as `write_policy::SpinlockGateStore`. Cross-thread sharing
706 /// pollutes MAPS, never memory: the lock serializes access.
707 struct SpinlockTouchLog {
708 lock: AtomicBool,
709 cell: UnsafeCell<TouchLog>,
710 }
711
712 // SAFETY: all access to `cell` goes through the `lock`
713 // acquire/release pair in `with_log`, so no two threads ever
714 // hold the interior reference at once.
715 unsafe impl Sync for SpinlockTouchLog {}
716
717 static LOG: SpinlockTouchLog = SpinlockTouchLog {
718 lock: AtomicBool::new(false),
719 cell: UnsafeCell::new(TouchLog::new()),
720 };
721
722 pub(super) fn with_log<R>(f: impl FnOnce(&mut TouchLog) -> R) -> R {
723 while LOG
724 .lock
725 .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
726 .is_err()
727 {
728 core::hint::spin_loop();
729 }
730 // SAFETY: the acquire CAS above grants exclusive access
731 // until the release store below; the closures passed here
732 // never re-enter this module.
733 let result = f(unsafe { &mut *LOG.cell.get() });
734 LOG.lock.store(false, Ordering::Release);
735 result
736 }
737 }
738
739 use store::with_log;
740}
741
742impl Default for SegmentBorrowRegistry {
743 #[inline(always)]
744 fn default() -> Self {
745 Self::new()
746 }
747}
748
749impl SegmentBorrowRegistry {
750 /// Create an empty registry.
751 #[inline(always)]
752 pub const fn new() -> Self {
753 const EMPTY: MaybeUninit<SegmentBorrow> = MaybeUninit::uninit();
754 Self {
755 entries: [EMPTY; MAX_SEGMENT_BORROWS],
756 len: 0,
757 }
758 }
759
760 /// Record `borrow` in the instruction-ambient touch log (see
761 /// [`touch_log`]), deduplicating by exact
762 /// `(key, offset, size, kind)` identity so repeated sequential
763 /// leases of the same range appear once. Once the log is full it
764 /// coalesces exact unions instead of truncating, so completeness
765 /// outlives granularity.
766 #[cfg(feature = "touch-map")]
767 #[inline]
768 fn record_touch(&mut self, borrow: &SegmentBorrow) {
769 touch_log::record(borrow);
770 }
771
772 /// Record a whole-account borrow as a `(0, data_len)` entry in the
773 /// touch log **without** registering a live-ledger entry
774 /// (`touch-map` feature).
775 ///
776 /// Whole-account borrows (`try_borrow_mut` / `load_mut`) are
777 /// governed by the account-level borrow byte, which already
778 /// mutually excludes live segment leases (segment acquires take
779 /// shared account borrows; the whole-account path takes the
780 /// exclusive one). Their *liveness* therefore never belongs in this
781 /// registry, only their cumulative footprint does. Since the log
782 /// moved to ambient storage, `AccountView::try_borrow_mut` records
783 /// this footprint itself; the method remains for callers that hold
784 /// a registry and want to stamp a footprint explicitly.
785 #[cfg(feature = "touch-map")]
786 #[inline]
787 pub fn record_account_touch(&mut self, key: &Address, data_len: u32, kind: AccessKind) {
788 touch_log::record_account(key, data_len, kind);
789 }
790
791 /// Visit every distinct range this instruction has touched so far
792 /// (`touch-map` feature). Order is first-touch order. Use
793 /// [`touch_map_overflowed`](Self::touch_map_overflowed) to detect a
794 /// partial log.
795 #[cfg(feature = "touch-map")]
796 #[inline]
797 pub fn for_each_touch<F: FnMut(&SegmentBorrow)>(&self, f: F) {
798 touch_log::for_each(f);
799 }
800
801 /// Number of distinct touch records captured (`touch-map` feature).
802 #[cfg(feature = "touch-map")]
803 #[inline(always)]
804 pub fn touch_map_len(&self) -> usize {
805 touch_log::len()
806 }
807
808 /// Whether the touch log dropped a range, more than
809 /// [`MAX_TOUCH_RECORDS`] pairwise-unmergeable ranges were touched,
810 /// and the map is therefore partial (`touch-map` feature). Full
811 /// logs coalesce exact unions before ever reporting `true` here.
812 #[cfg(feature = "touch-map")]
813 #[inline(always)]
814 pub fn touch_map_overflowed(&self) -> bool {
815 touch_log::overflowed()
816 }
817
818 /// Number of active borrows.
819 #[inline(always)]
820 pub const fn len(&self) -> usize {
821 self.len as usize
822 }
823
824 /// Whether the registry is empty.
825 #[inline(always)]
826 pub const fn is_empty(&self) -> bool {
827 self.len == 0
828 }
829
830 /// Register a new read borrow and return the `SegmentBorrow`
831 /// record the caller can hand to `SegmentLease::new` for RAII
832 /// release. This is the plumbing that makes
833 /// [`crate::segment_lease::SegRef`] possible.
834 #[inline(always)]
835 pub fn register_leased_read(
836 &mut self,
837 key: &Address,
838 offset: u32,
839 size: u32,
840 ) -> Result<SegmentBorrow, ProgramError> {
841 let borrow = SegmentBorrow {
842 key_fp: address_fingerprint(key),
843 key: *key,
844 offset,
845 size,
846 kind: AccessKind::Read,
847 };
848 self.register(borrow)?;
849 Ok(borrow)
850 }
851
852 /// Mutable counterpart of [`Self::register_leased_read`].
853 #[inline(always)]
854 pub fn register_leased_write(
855 &mut self,
856 key: &Address,
857 offset: u32,
858 size: u32,
859 ) -> Result<SegmentBorrow, ProgramError> {
860 let borrow = SegmentBorrow {
861 key_fp: address_fingerprint(key),
862 key: *key,
863 offset,
864 size,
865 kind: AccessKind::Write,
866 };
867 self.register(borrow)?;
868 Ok(borrow)
869 }
870
871 /// Register a new segment borrow, checking for conflicts.
872 ///
873 /// Returns `Err(AccountBorrowFailed)` if the new borrow overlaps an
874 /// existing borrow with incompatible access (read+write or write+write)
875 /// on the **same** account (full-address identity, not fingerprint).
876 #[inline(always)]
877 pub fn register(&mut self, new: SegmentBorrow) -> Result<(), ProgramError> {
878 let len = self.len as usize;
879 if len >= MAX_SEGMENT_BORROWS {
880 return Err(ProgramError::AccountBorrowFailed);
881 }
882
883 // Check conflicts against all active borrows. Fast path on the
884 // 8-byte fingerprint; slow path confirms with the full 32-byte
885 // address so fingerprint collisions cannot manufacture false
886 // conflicts between unrelated accounts.
887 let mut i = 0;
888 while i < len {
889 // SAFETY: `i < len`, and every slot below `len` was initialized by
890 // `register` before `self.len` was advanced.
891 let existing = unsafe { self.entries.get_unchecked(i).assume_init_ref() };
892 if existing.key_fp == new.key_fp
893 && address_eq(&existing.key, &new.key)
894 && ranges_overlap(existing.offset, existing.size, new.offset, new.size)
895 {
896 match (existing.kind, new.kind) {
897 (AccessKind::Read, AccessKind::Read) => {}
898 _ => return Err(ProgramError::AccountBorrowFailed),
899 }
900 }
901 i += 1;
902 }
903
904 // SAFETY: Capacity was checked above, so `len` is an in-bounds
905 // uninitialized slot owned by this registry.
906 unsafe { self.entries.get_unchecked_mut(len).write(new) };
907 self.len = (len + 1) as u8;
908 // Record the successful registration in the touch map.
909 // append-only log. Releases never remove touch records, the log
910 // is the instruction's cumulative footprint.
911 #[cfg(feature = "touch-map")]
912 self.record_touch(&new);
913 Ok(())
914 }
915
916 /// Convenience: register a read borrow for the given account region.
917 #[inline(always)]
918 pub fn register_read(
919 &mut self,
920 key: &Address,
921 offset: u32,
922 size: u32,
923 ) -> Result<(), ProgramError> {
924 self.register(SegmentBorrow {
925 key_fp: address_fingerprint(key),
926 key: *key,
927 offset,
928 size,
929 kind: AccessKind::Read,
930 })
931 }
932
933 /// Convenience: register a write borrow for the given account region.
934 #[inline(always)]
935 pub fn register_write(
936 &mut self,
937 key: &Address,
938 offset: u32,
939 size: u32,
940 ) -> Result<(), ProgramError> {
941 self.register(SegmentBorrow {
942 key_fp: address_fingerprint(key),
943 key: *key,
944 offset,
945 size,
946 kind: AccessKind::Write,
947 })
948 }
949
950 /// Release a previously registered borrow.
951 ///
952 /// Finds the first matching entry and removes it, compacting the array.
953 /// Identity is full-address (not fingerprint) to stay collision-safe.
954 #[inline(always)]
955 pub fn release(&mut self, borrow: &SegmentBorrow) -> bool {
956 let len = self.len as usize;
957 let mut i = 0;
958 while i < len {
959 // SAFETY: `i < len`, and all slots below `len` are initialized.
960 let existing = unsafe { self.entries.get_unchecked(i).assume_init_ref() };
961 if borrow_eq(existing, borrow) {
962 // Swap-remove: move last entry into this slot.
963 let new_len = len - 1;
964 self.len = new_len as u8;
965 if i < new_len {
966 // SAFETY: `new_len < len`, so the former last entry is
967 // initialized and available to move into the removed slot.
968 let last = unsafe { self.entries.get_unchecked(new_len).assume_init() };
969 // SAFETY: `i < new_len`, so the target slot is in-bounds.
970 unsafe { self.entries.get_unchecked_mut(i).write(last) };
971 }
972 return true;
973 }
974 i += 1;
975 }
976 false
977 }
978
979 /// Release a borrow that is expected to be the most recently registered one.
980 ///
981 /// The last slot is checked first for the hot RAII cleanup path. If the
982 /// entry is no longer last, this falls back to exact removal instead of
983 /// popping an unrelated borrow.
984 ///
985 /// # Safety
986 ///
987 /// The caller must ensure `borrow` was previously registered in this
988 /// registry and that calling this does not violate any higher-level aliasing
989 /// contract.
990 #[doc(hidden)]
991 #[inline(always)]
992 pub unsafe fn release_last_registered(&mut self, borrow: &SegmentBorrow) -> bool {
993 let len = self.len as usize;
994 if len == 0 {
995 return false;
996 }
997 // SAFETY: `len > 0`, and all slots below `len` are initialized.
998 let last = unsafe { *self.entries.get_unchecked(len - 1).assume_init_ref() };
999 if !borrow_eq(&last, borrow) {
1000 return self.release(borrow);
1001 }
1002 self.len = (len - 1) as u8;
1003 true
1004 }
1005
1006 /// Reset the registry, clearing all active borrows.
1007 #[inline(always)]
1008 pub fn clear(&mut self) {
1009 self.len = 0;
1010 }
1011
1012 /// Check if a proposed borrow would conflict, without registering it.
1013 ///
1014 /// Uses full-address identity, fingerprint collisions do not
1015 /// produce false positives.
1016 #[inline(always)]
1017 pub fn would_conflict(&self, proposed: &SegmentBorrow) -> bool {
1018 let len = self.len as usize;
1019 let mut i = 0;
1020 while i < len {
1021 // SAFETY: `i < len`, and all slots below `len` are initialized.
1022 let existing = unsafe { self.entries.get_unchecked(i).assume_init_ref() };
1023 if existing.key_fp == proposed.key_fp
1024 && address_eq(&existing.key, &proposed.key)
1025 && ranges_overlap(
1026 existing.offset,
1027 existing.size,
1028 proposed.offset,
1029 proposed.size,
1030 )
1031 {
1032 match (existing.kind, proposed.kind) {
1033 (AccessKind::Read, AccessKind::Read) => {}
1034 _ => return true,
1035 }
1036 }
1037 i += 1;
1038 }
1039 false
1040 }
1041
1042 /// Register a borrow and return an RAII guard that auto-releases it on drop.
1043 ///
1044 /// This is the preferred way to acquire segment borrows, the guard
1045 /// ensures the borrow is released even if the caller returns early
1046 /// via `?` or encounters an error.
1047 ///
1048 /// # Example
1049 ///
1050 /// ```ignore
1051 /// {
1052 /// let _guard = borrows.register_guard_write(&key, 0, 8)?;
1053 /// // ... write to segment ...
1054 /// } // guard dropped → borrow released
1055 /// ```
1056 #[inline(always)]
1057 pub fn register_guard(
1058 &mut self,
1059 borrow: SegmentBorrow,
1060 ) -> Result<SegmentBorrowGuard<'_>, ProgramError> {
1061 self.register(borrow)?;
1062 Ok(SegmentBorrowGuard {
1063 registry: self,
1064 borrow,
1065 })
1066 }
1067
1068 /// Register a read borrow with RAII auto-release.
1069 #[inline(always)]
1070 pub fn register_guard_read(
1071 &mut self,
1072 key: &Address,
1073 offset: u32,
1074 size: u32,
1075 ) -> Result<SegmentBorrowGuard<'_>, ProgramError> {
1076 let borrow = SegmentBorrow {
1077 key_fp: address_fingerprint(key),
1078 key: *key,
1079 offset,
1080 size,
1081 kind: AccessKind::Read,
1082 };
1083 self.register_guard(borrow)
1084 }
1085
1086 /// Register a write borrow with RAII auto-release.
1087 #[inline(always)]
1088 pub fn register_guard_write(
1089 &mut self,
1090 key: &Address,
1091 offset: u32,
1092 size: u32,
1093 ) -> Result<SegmentBorrowGuard<'_>, ProgramError> {
1094 let borrow = SegmentBorrow {
1095 key_fp: address_fingerprint(key),
1096 key: *key,
1097 offset,
1098 size,
1099 kind: AccessKind::Write,
1100 };
1101 self.register_guard(borrow)
1102 }
1103
1104 /// Visit each active borrow in registration order.
1105 ///
1106 /// Intended for diagnostics and for the `hopper explain`
1107 /// introspection path, never for hot-path decisions.
1108 #[inline]
1109 pub fn for_each<F: FnMut(&SegmentBorrow)>(&self, mut f: F) {
1110 let len = self.len as usize;
1111 let mut i = 0;
1112 while i < len {
1113 // SAFETY: `i < len`, and all slots below `len` are initialized.
1114 f(unsafe { self.entries.get_unchecked(i).assume_init_ref() });
1115 i += 1;
1116 }
1117 }
1118
1119 /// Look up an active borrow by exact `(key, offset, size, kind)`.
1120 #[inline]
1121 pub fn find_exact(
1122 &self,
1123 key: &Address,
1124 offset: u32,
1125 size: u32,
1126 kind: AccessKind,
1127 ) -> Option<&SegmentBorrow> {
1128 let fp = address_fingerprint(key);
1129 let len = self.len as usize;
1130 let mut i = 0;
1131 while i < len {
1132 // SAFETY: `i < len`, and all slots below `len` are initialized.
1133 let e = unsafe { self.entries.get_unchecked(i).assume_init_ref() };
1134 if e.key_fp == fp
1135 && address_eq(&e.key, key)
1136 && e.offset == offset
1137 && e.size == size
1138 && e.kind == kind
1139 {
1140 return Some(e);
1141 }
1142 i += 1;
1143 }
1144 None
1145 }
1146}
1147
1148/// RAII guard that releases a segment borrow when dropped.
1149///
1150/// Created by [`SegmentBorrowRegistry::register_guard()`] and its
1151/// convenience wrappers. The borrow is automatically released from the
1152/// registry on drop, preventing borrow leaks.
1153pub struct SegmentBorrowGuard<'a> {
1154 registry: &'a mut SegmentBorrowRegistry,
1155 borrow: SegmentBorrow,
1156}
1157
1158impl<'a> SegmentBorrowGuard<'a> {
1159 /// Access kind of the guarded borrow.
1160 #[inline(always)]
1161 pub fn kind(&self) -> AccessKind {
1162 self.borrow.kind
1163 }
1164
1165 /// Byte offset of the guarded segment.
1166 #[inline(always)]
1167 pub fn offset(&self) -> u32 {
1168 self.borrow.offset
1169 }
1170
1171 /// Byte size of the guarded segment.
1172 #[inline(always)]
1173 pub fn size(&self) -> u32 {
1174 self.borrow.size
1175 }
1176}
1177
1178impl<'a> Drop for SegmentBorrowGuard<'a> {
1179 fn drop(&mut self) {
1180 self.registry.release(&self.borrow);
1181 }
1182}
1183
1184#[cfg(kani)]
1185mod kani_proofs {
1186 use super::*;
1187
1188 #[kani::proof]
1189 fn range_overlap_is_symmetric_for_arbitrary_u32s() {
1190 let a_off: u32 = kani::any();
1191 let a_size: u32 = kani::any();
1192 let b_off: u32 = kani::any();
1193 let b_size: u32 = kani::any();
1194
1195 assert_eq!(
1196 ranges_overlap(a_off, a_size, b_off, b_size),
1197 ranges_overlap(b_off, b_size, a_off, a_size)
1198 );
1199 }
1200
1201 #[kani::proof]
1202 fn overlapping_write_blocks_same_account_accesses() {
1203 let offset: u32 = kani::any();
1204 let size: u32 = kani::any();
1205 let delta: u32 = kani::any();
1206 kani::assume(offset <= 1024);
1207 kani::assume(size > 0 && size <= 64);
1208 kani::assume(delta < size);
1209
1210 let key = Address::new([7u8; 32]);
1211 let probe_offset = offset + delta;
1212 let mut reg = SegmentBorrowRegistry::new();
1213
1214 assert!(reg.register_write(&key, offset, size).is_ok());
1215 assert!(reg.register_read(&key, probe_offset, 1).is_err());
1216 assert!(reg.register_write(&key, probe_offset, 1).is_err());
1217 assert_eq!(reg.len(), 1);
1218 }
1219
1220 #[kani::proof]
1221 fn overlapping_reads_are_shared_for_same_account() {
1222 let offset: u32 = kani::any();
1223 let size: u32 = kani::any();
1224 let delta: u32 = kani::any();
1225 kani::assume(offset <= 1024);
1226 kani::assume(size > 0 && size <= 64);
1227 kani::assume(delta < size);
1228
1229 let key = Address::new([8u8; 32]);
1230 let probe_offset = offset + delta;
1231 let mut reg = SegmentBorrowRegistry::new();
1232
1233 assert!(reg.register_read(&key, offset, size).is_ok());
1234 assert!(reg.register_read(&key, probe_offset, 1).is_ok());
1235 assert_eq!(reg.len(), 2);
1236 }
1237
1238 #[kani::proof]
1239 fn fingerprint_collision_different_addresses_do_not_conflict() {
1240 let key_a = Address::new([9u8; 32]);
1241 let mut key_b_bytes = [9u8; 32];
1242 key_b_bytes[8] = 10;
1243 let key_b = Address::new(key_b_bytes);
1244 let mut reg = SegmentBorrowRegistry::new();
1245
1246 assert_eq!(address_fingerprint(&key_a), address_fingerprint(&key_b));
1247 assert_ne!(key_a.as_array(), key_b.as_array());
1248 assert!(reg.register_write(&key_a, 0, 8).is_ok());
1249 assert!(reg.register_write(&key_b, 0, 8).is_ok());
1250 assert_eq!(reg.len(), 2);
1251 }
1252
1253 #[kani::proof]
1254 fn release_removes_exact_borrow_and_preserves_others() {
1255 let key = Address::new([11u8; 32]);
1256 let mut reg = SegmentBorrowRegistry::new();
1257
1258 let first = reg.register_leased_read(&key, 0, 8).unwrap();
1259 let second = reg.register_leased_write(&key, 8, 8).unwrap();
1260 assert!(reg.release(&first));
1261
1262 assert_eq!(reg.len(), 1);
1263 assert!(reg.find_exact(&key, 0, 8, AccessKind::Read).is_none());
1264 assert!(reg.find_exact(&key, 8, 8, AccessKind::Write).is_some());
1265 assert!(reg.release(&second));
1266 assert!(reg.is_empty());
1267 }
1268}
1269
1270// ── Tests ────────────────────────────────────────────────────────────
1271
1272#[cfg(test)]
1273mod tests {
1274 use super::*;
1275 use crate::Address;
1276
1277 fn test_addr(seed: u8) -> Address {
1278 Address::new([seed; 32])
1279 }
1280
1281 #[test]
1282 fn read_read_same_range_allowed() {
1283 let mut reg = SegmentBorrowRegistry::new();
1284 let key = test_addr(1);
1285 assert!(reg.register_read(&key, 0, 8).is_ok());
1286 assert!(reg.register_read(&key, 0, 8).is_ok());
1287 assert_eq!(reg.len(), 2);
1288 }
1289
1290 #[test]
1291 fn read_write_same_range_rejected() {
1292 let mut reg = SegmentBorrowRegistry::new();
1293 let key = test_addr(1);
1294 assert!(reg.register_read(&key, 0, 8).is_ok());
1295 assert!(reg.register_write(&key, 0, 8).is_err());
1296 }
1297
1298 #[test]
1299 fn write_write_same_range_rejected() {
1300 let mut reg = SegmentBorrowRegistry::new();
1301 let key = test_addr(1);
1302 assert!(reg.register_write(&key, 0, 8).is_ok());
1303 assert!(reg.register_write(&key, 0, 8).is_err());
1304 }
1305
1306 #[test]
1307 fn write_read_same_range_rejected() {
1308 let mut reg = SegmentBorrowRegistry::new();
1309 let key = test_addr(1);
1310 assert!(reg.register_write(&key, 0, 8).is_ok());
1311 assert!(reg.register_read(&key, 0, 8).is_err());
1312 }
1313
1314 #[test]
1315 fn non_overlapping_write_write_allowed() {
1316 let mut reg = SegmentBorrowRegistry::new();
1317 let key = test_addr(1);
1318 // balance: [0..8), metadata: [8..40)
1319 assert!(reg.register_write(&key, 0, 8).is_ok());
1320 assert!(reg.register_write(&key, 8, 32).is_ok());
1321 }
1322
1323 #[test]
1324 fn partially_overlapping_rejected() {
1325 let mut reg = SegmentBorrowRegistry::new();
1326 let key = test_addr(1);
1327 // [0..16) and [8..24) overlap at [8..16)
1328 assert!(reg.register_write(&key, 0, 16).is_ok());
1329 assert!(reg.register_write(&key, 8, 16).is_err());
1330 }
1331
1332 #[test]
1333 fn different_accounts_always_allowed() {
1334 let mut reg = SegmentBorrowRegistry::new();
1335 assert!(reg.register_write(&test_addr(1), 0, 8).is_ok());
1336 assert!(reg.register_write(&test_addr(2), 0, 8).is_ok());
1337 }
1338
1339 #[test]
1340 fn release_then_reacquire() {
1341 let mut reg = SegmentBorrowRegistry::new();
1342 let key = test_addr(1);
1343 let borrow = SegmentBorrow {
1344 key_fp: address_fingerprint(&key),
1345 key,
1346 offset: 0,
1347 size: 8,
1348 kind: AccessKind::Write,
1349 };
1350 assert!(reg.register(borrow).is_ok());
1351 assert!(reg.register_write(&key, 0, 8).is_err()); // conflict
1352 assert!(reg.release(&borrow));
1353 assert!(reg.register_write(&key, 0, 8).is_ok()); // now OK
1354 }
1355
1356 #[test]
1357 fn release_last_registered_falls_back_to_exact_release() {
1358 let mut reg = SegmentBorrowRegistry::new();
1359 let key = test_addr(1);
1360 let first = reg.register_leased_read(&key, 0, 8).unwrap();
1361 let second = reg.register_leased_write(&key, 8, 8).unwrap();
1362
1363 // SAFETY: `first` was returned by this registry and has not been
1364 // released yet.
1365 assert!(unsafe { reg.release_last_registered(&first) });
1366 assert_eq!(reg.len(), 1);
1367 assert!(reg.find_exact(&key, 0, 8, AccessKind::Read).is_none());
1368 assert!(reg.find_exact(&key, 8, 8, AccessKind::Write).is_some());
1369
1370 // SAFETY: `second` was returned by this registry and has not been
1371 // released yet.
1372 assert!(unsafe { reg.release_last_registered(&second) });
1373 assert!(reg.is_empty());
1374 }
1375
1376 #[test]
1377 fn capacity_limit() {
1378 let mut reg = SegmentBorrowRegistry::new();
1379 for i in 0..MAX_SEGMENT_BORROWS {
1380 assert!(reg.register_read(&test_addr(1), i as u32 * 8, 8).is_ok());
1381 }
1382 // One more should fail.
1383 assert!(reg.register_read(&test_addr(1), 256, 8).is_err());
1384 }
1385
1386 #[test]
1387 fn would_conflict_does_not_mutate() {
1388 let mut reg = SegmentBorrowRegistry::new();
1389 let key = test_addr(1);
1390 assert!(reg.register_write(&key, 0, 8).is_ok());
1391 let proposed = SegmentBorrow {
1392 key_fp: address_fingerprint(&key),
1393 key,
1394 offset: 0,
1395 size: 8,
1396 kind: AccessKind::Write,
1397 };
1398 assert!(reg.would_conflict(&proposed));
1399 assert_eq!(reg.len(), 1); // unchanged
1400 }
1401
1402 #[test]
1403 fn adjacent_ranges_no_conflict() {
1404 let mut reg = SegmentBorrowRegistry::new();
1405 let key = test_addr(1);
1406 // [0..8) and [8..16) are adjacent, not overlapping.
1407 assert!(reg.register_write(&key, 0, 8).is_ok());
1408 assert!(reg.register_write(&key, 8, 8).is_ok());
1409 }
1410
1411 // ── SegmentBorrowGuard RAII tests ────────────────────────────────
1412 //
1413 // The guard holds `&mut SegmentBorrowRegistry`, which provides
1414 // compile-time exclusion: the borrow checker prevents any registry
1415 // access while a guard is alive, giving *stronger* protection than
1416 // runtime conflict checks alone. Tests verify the auto-release
1417 // behavior by inspecting the registry after the guard drops.
1418
1419 #[test]
1420 fn guard_auto_releases_write_on_drop() {
1421 let mut reg = SegmentBorrowRegistry::new();
1422 let key = test_addr(1);
1423 {
1424 let _guard = reg.register_guard_write(&key, 0, 8).unwrap();
1425 // guard alive, registry exclusively borrowed at compile time
1426 }
1427 // After drop: slot freed, len back to 0.
1428 assert_eq!(reg.len(), 0);
1429 // Re-acquire the same range, proves release happened.
1430 assert!(reg.register_write(&key, 0, 8).is_ok());
1431 }
1432
1433 #[test]
1434 fn guard_auto_releases_read_on_drop() {
1435 let mut reg = SegmentBorrowRegistry::new();
1436 let key = test_addr(1);
1437 {
1438 let _guard = reg.register_guard_read(&key, 0, 8).unwrap();
1439 }
1440 assert_eq!(reg.len(), 0);
1441 // Write now succeeds, the read borrow was released.
1442 assert!(reg.register_write(&key, 0, 8).is_ok());
1443 }
1444
1445 #[test]
1446 fn sequential_guards_reuse_slot() {
1447 let mut reg = SegmentBorrowRegistry::new();
1448 let key = test_addr(1);
1449 for _ in 0..4 {
1450 let _guard = reg.register_guard_write(&key, 0, 8).unwrap();
1451 // each iteration: acquire, drop at end of loop body
1452 }
1453 assert_eq!(reg.len(), 0);
1454 }
1455
1456 #[test]
1457 fn guard_accessors() {
1458 let mut reg = SegmentBorrowRegistry::new();
1459 let key = test_addr(1);
1460 let guard = reg.register_guard_write(&key, 16, 32).unwrap();
1461 assert_eq!(guard.kind(), AccessKind::Write);
1462 assert_eq!(guard.offset(), 16);
1463 assert_eq!(guard.size(), 32);
1464 }
1465
1466 #[test]
1467 fn guard_then_manual_register_ok() {
1468 let mut reg = SegmentBorrowRegistry::new();
1469 let key = test_addr(1);
1470 {
1471 let _guard = reg.register_guard_write(&key, 0, 8).unwrap();
1472 }
1473 // Guard released, manual register on overlapping range works.
1474 assert!(reg.register_read(&key, 0, 8).is_ok());
1475 assert_eq!(reg.len(), 1);
1476 }
1477}
1478
1479// ── Property tests ───────────────────────────────────────────────────
1480//
1481// The kani proofs above verify the overlap predicate and a few hand-
1482// chosen registry sequences exhaustively. These property tests carve
1483// *randomly generated* segment maps and assert the registry's runtime
1484// behaviour against an independent reference oracle, so a regression in
1485// the conflict scan is caught even on inputs nobody thought to write a
1486// unit test for. proptest is a host-only dev-dependency; this module is
1487// `#[cfg(test)]` and never reaches the no_std SBF build.
1488#[cfg(test)]
1489mod proptests {
1490 use super::*;
1491 use crate::Address;
1492 use proptest::prelude::*;
1493
1494 /// A single carved write segment: `[offset, offset + size)`.
1495 #[derive(Debug, Clone, Copy)]
1496 struct Seg {
1497 offset: u32,
1498 size: u32,
1499 }
1500
1501 /// Reference overlap check, written independently of the production
1502 /// `ranges_overlap` so the two can disagree and surface a bug.
1503 fn oracle_overlap(a: Seg, b: Seg) -> bool {
1504 let a_end = a.offset as u64 + a.size as u64;
1505 let b_end = b.offset as u64 + b.size as u64;
1506 (a.offset as u64) < b_end && (b.offset as u64) < a_end
1507 }
1508
1509 // Small, dense ranges so collisions actually happen and we exercise
1510 // both the accept and reject paths. Sizes are >= 1 (a zero-size
1511 // borrow can never overlap and is not interesting here).
1512 fn seg_strategy() -> impl Strategy<Value = Seg> {
1513 (0u32..64, 1u32..16).prop_map(|(offset, size)| Seg { offset, size })
1514 }
1515
1516 proptest! {
1517 /// Registering a sequence of write borrows on the *same* account
1518 /// must accept exactly the segments that are disjoint from every
1519 /// previously-accepted segment, and reject every one that
1520 /// overlaps an accepted segment. We replay the same decisions on
1521 /// an independent oracle and require they agree.
1522 #[test]
1523 fn write_borrows_match_disjointness_oracle(
1524 segs in proptest::collection::vec(seg_strategy(), 0..MAX_SEGMENT_BORROWS)
1525 ) {
1526 let key = Address::new([42u8; 32]);
1527 let mut reg = SegmentBorrowRegistry::new();
1528 let mut accepted: alloc_vec::Vec<Seg> = alloc_vec::Vec::new();
1529
1530 for seg in segs {
1531 let conflicts = accepted.iter().any(|prev| oracle_overlap(*prev, seg));
1532 let result = reg.register_write(&key, seg.offset, seg.size);
1533 if conflicts {
1534 prop_assert!(
1535 result.is_err(),
1536 "registry accepted an overlapping write {:?} against {:?}",
1537 seg,
1538 accepted
1539 );
1540 } else {
1541 prop_assert!(
1542 result.is_ok(),
1543 "registry rejected a disjoint write {:?} against {:?}",
1544 seg,
1545 accepted
1546 );
1547 accepted.push(seg);
1548 }
1549 }
1550 prop_assert_eq!(reg.len(), accepted.len());
1551 }
1552
1553 /// Overlapping *reads* are always shareable: a read never
1554 /// conflicts with another read regardless of how the ranges are
1555 /// carved, so every read in the sequence must be accepted (up to
1556 /// the capacity bound, which the input size respects).
1557 #[test]
1558 fn read_borrows_never_conflict(
1559 segs in proptest::collection::vec(seg_strategy(), 0..MAX_SEGMENT_BORROWS)
1560 ) {
1561 let key = Address::new([7u8; 32]);
1562 let mut reg = SegmentBorrowRegistry::new();
1563 let n = segs.len();
1564 for seg in segs {
1565 prop_assert!(reg.register_read(&key, seg.offset, seg.size).is_ok());
1566 }
1567 prop_assert_eq!(reg.len(), n);
1568 }
1569
1570 /// Borrows on distinct accounts never conflict, even when their
1571 /// byte ranges are identical: disjointness is per-account.
1572 #[test]
1573 fn distinct_accounts_never_conflict(seg in seg_strategy()) {
1574 let mut reg = SegmentBorrowRegistry::new();
1575 prop_assert!(reg.register_write(&Address::new([1u8; 32]), seg.offset, seg.size).is_ok());
1576 prop_assert!(reg.register_write(&Address::new([2u8; 32]), seg.offset, seg.size).is_ok());
1577 prop_assert_eq!(reg.len(), 2);
1578 }
1579 }
1580
1581 // The registry is no_std and never allocates; the proptest oracle is
1582 // host-only, so a plain `std::vec::Vec` is fine for bookkeeping.
1583 mod alloc_vec {
1584 pub use std::vec::Vec;
1585 }
1586}
1587
1588/// Host-only decode twin of [`encode_touch_map`], shared by the
1589/// round-trip tests here and in `context.rs`. Mirrors the validation an
1590/// off-chain consumer must perform: magic, version, and the exact-length
1591/// equation `len == 4 + 9 * count`.
1592#[cfg(all(test, feature = "touch-map"))]
1593pub(crate) fn decode_touch_map_for_tests(
1594 bytes: &[u8],
1595) -> Option<(u8, std::vec::Vec<TouchMapRecord>)> {
1596 if bytes.len() < TOUCH_MAP_HEADER_LEN {
1597 return None;
1598 }
1599 if bytes[0] != TOUCH_MAP_MAGIC || bytes[1] != TOUCH_MAP_VERSION {
1600 return None;
1601 }
1602 let flags = bytes[2];
1603 let count = bytes[3] as usize;
1604 if bytes.len() != TOUCH_MAP_HEADER_LEN + count * TOUCH_MAP_RECORD_LEN {
1605 return None;
1606 }
1607 let mut records = std::vec::Vec::with_capacity(count);
1608 for i in 0..count {
1609 let base = TOUCH_MAP_HEADER_LEN + i * TOUCH_MAP_RECORD_LEN;
1610 let packed = u32::from_le_bytes(bytes[base + 1..base + 5].try_into().unwrap());
1611 records.push(TouchMapRecord {
1612 slot: bytes[base],
1613 offset: packed & 0x7FFF_FFFF,
1614 size: u32::from_le_bytes(bytes[base + 5..base + 9].try_into().unwrap()),
1615 write: packed & 0x8000_0000 != 0,
1616 });
1617 }
1618 Some((flags, records))
1619}
1620
1621#[cfg(all(test, feature = "touch-map"))]
1622mod touch_map_tests {
1623 use super::*;
1624
1625 fn key(byte: u8) -> Address {
1626 Address::new([byte; 32])
1627 }
1628
1629 #[test]
1630 fn touch_log_survives_release_and_dedups() {
1631 let mut reg = SegmentBorrowRegistry::new();
1632
1633 // Two disjoint borrows on one account, one on another.
1634 let a = reg.register_leased_write(&key(1), 0, 8).unwrap();
1635 let b = reg.register_leased_read(&key(1), 8, 8).unwrap();
1636 let c = reg.register_leased_read(&key(2), 0, 4).unwrap();
1637
1638 // Release everything (the RAII path): the live ledger empties...
1639 assert!(reg.release(&a));
1640 assert!(reg.release(&b));
1641 assert!(reg.release(&c));
1642 assert!(reg.is_empty());
1643
1644 // ...but the touch log keeps the cumulative footprint.
1645 assert_eq!(reg.touch_map_len(), 3);
1646 assert!(!reg.touch_map_overflowed());
1647
1648 // Re-registering an identical range (sequential lease) dedups.
1649 let a2 = reg.register_leased_write(&key(1), 0, 8).unwrap();
1650 assert_eq!(reg.touch_map_len(), 3);
1651 // A same-range borrow with a different kind is a distinct record.
1652 reg.release(&a2);
1653 let _a3 = reg.register_leased_read(&key(1), 0, 8).unwrap();
1654 assert_eq!(reg.touch_map_len(), 4);
1655
1656 // First-touch order is preserved.
1657 let mut seen = std::vec::Vec::new();
1658 reg.for_each_touch(|t| seen.push((t.key, t.offset, t.size, t.kind)));
1659 assert_eq!(seen[0], (key(1), 0, 8, AccessKind::Write));
1660 assert_eq!(seen[1], (key(1), 8, 8, AccessKind::Read));
1661 assert_eq!(seen[2], (key(2), 0, 4, AccessKind::Read));
1662 assert_eq!(seen[3], (key(1), 0, 8, AccessKind::Read));
1663 }
1664
1665 #[test]
1666 fn whole_account_touch_recorded_without_live_ledger_entry() {
1667 let mut reg = SegmentBorrowRegistry::new();
1668
1669 // A segment lease and a whole-account borrow on the same account.
1670 let seg = reg.register_leased_write(&key(3), 16, 8).unwrap();
1671 reg.release(&seg);
1672 reg.record_account_touch(&key(3), 64, AccessKind::Write);
1673
1674 // The whole-account record is footprint-only: the live ledger
1675 // stays empty (the account borrow byte owns its liveness), so a
1676 // later segment lease on the same bytes is not falsely blocked.
1677 assert!(reg.is_empty());
1678 assert!(reg.register_write(&key(3), 0, 8).is_ok());
1679
1680 // Both access shapes appear in the touch map, and repeating the
1681 // whole-account borrow dedups.
1682 reg.record_account_touch(&key(3), 64, AccessKind::Write);
1683 assert_eq!(reg.touch_map_len(), 3);
1684 let mut seen = std::vec::Vec::new();
1685 reg.for_each_touch(|t| seen.push((t.offset, t.size, t.kind)));
1686 assert_eq!(seen[0], (16, 8, AccessKind::Write));
1687 assert_eq!(seen[1], (0, 64, AccessKind::Write));
1688 assert_eq!(seen[2], (0, 8, AccessKind::Write));
1689 }
1690
1691 #[test]
1692 fn touch_log_flags_overflow_and_stays_partial_not_wrong() {
1693 let mut reg = SegmentBorrowRegistry::new();
1694 // Touch more distinct ranges than the log holds. Register/release
1695 // pairs keep the live ledger small while the touch log accumulates.
1696 // The stride leaves an 8-byte gap between consecutive ranges, so no
1697 // exact union exists and coalescing cannot save the map, the
1698 // honest outcome is a flagged partial log.
1699 let mut i: u32 = 0;
1700 while (i as usize) < MAX_TOUCH_RECORDS + 3 {
1701 let b = reg.register_leased_read(&key(9), i * 16, 8).unwrap();
1702 reg.release(&b);
1703 i += 1;
1704 }
1705 assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
1706 assert!(reg.touch_map_overflowed());
1707 }
1708
1709 /// The columnar pattern (Sentinel's `record_entry`, `Seq` pushes) at
1710 /// a scale the granular log cannot hold: contiguous cells must
1711 /// coalesce into exact unions, a COMPLETE, unflagged map, instead
1712 /// of truncating into a partial one.
1713 #[test]
1714 fn columnar_contiguous_writes_coalesce_instead_of_overflowing() {
1715 let mut reg = SegmentBorrowRegistry::new();
1716 let cells = MAX_TOUCH_RECORDS * 4;
1717 let mut i: u32 = 0;
1718 while (i as usize) < cells {
1719 let b = reg.register_leased_write(&key(7), i * 8, 8).unwrap();
1720 reg.release(&b);
1721 i += 1;
1722 }
1723 // Granularity degraded, coverage did not: no overflow flag, and
1724 // the records' union is exactly [0, cells * 8), no gap (nothing
1725 // touched went missing) and no byte beyond it (nothing untouched
1726 // was claimed).
1727 assert!(!reg.touch_map_overflowed());
1728 assert!(reg.touch_map_len() <= MAX_TOUCH_RECORDS);
1729 let total = cells as u64 * 8;
1730 let mut ranges = std::vec::Vec::new();
1731 reg.for_each_touch(|t| {
1732 assert_eq!(t.kind, AccessKind::Write);
1733 assert_eq!(t.key, key(7));
1734 let end = t.offset as u64 + t.size as u64;
1735 assert!(end <= total, "coalesced record claims untouched bytes");
1736 ranges.push((t.offset as u64, end));
1737 });
1738 ranges.sort_unstable();
1739 let mut covered_to = 0u64;
1740 for (start, end) in ranges {
1741 assert!(
1742 start <= covered_to,
1743 "gap in coalesced coverage at {covered_to}"
1744 );
1745 if end > covered_to {
1746 covered_to = end;
1747 }
1748 }
1749 assert_eq!(covered_to, total);
1750 }
1751
1752 /// Under pressure, a read wholly inside an existing write is
1753 /// absorbed (the write already claims strictly more access), while a
1754 /// read poking OUTSIDE the write must never vanish into it, that
1755 /// union would fake write access to bytes that were only read. With
1756 /// every slot pairwise-unmergeable, the honest outcome for the
1757 /// poking read is the overflow flag.
1758 #[test]
1759 fn pressure_absorbs_contained_reads_but_never_widens_a_write() {
1760 let mut reg = SegmentBorrowRegistry::new();
1761 let w = reg.register_leased_write(&key(1), 0, 64).unwrap();
1762 reg.release(&w);
1763 // Fill the remaining slots with gap-separated reads on another
1764 // account so nothing same-kind can merge.
1765 let mut i: u32 = 0;
1766 while (i as usize) < MAX_TOUCH_RECORDS - 1 {
1767 let b = reg.register_leased_read(&key(2), i * 16, 8).unwrap();
1768 reg.release(&b);
1769 i += 1;
1770 }
1771 assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
1772 assert!(!reg.touch_map_overflowed());
1773
1774 // Contained read: absorbed, still complete.
1775 let r = reg.register_leased_read(&key(1), 4, 4).unwrap();
1776 reg.release(&r);
1777 assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
1778 assert!(!reg.touch_map_overflowed());
1779
1780 // Read straddling the write's end: not absorbable, not
1781 // compactable, flagged partial, and the write stays EXACTLY as
1782 // acquired.
1783 let r2 = reg.register_leased_read(&key(1), 60, 8).unwrap();
1784 reg.release(&r2);
1785 assert!(reg.touch_map_overflowed());
1786 reg.for_each_touch(|t| {
1787 if t.kind == AccessKind::Write {
1788 assert_eq!((t.key, t.offset, t.size), (key(1), 0, 64));
1789 }
1790 });
1791 }
1792
1793 /// The reverse absorption: a write covering an already-recorded read
1794 /// upgrades that slot to the write, a kind that genuinely occurred,
1795 /// over a superset of the bytes, instead of overflowing.
1796 #[test]
1797 fn pressure_upgrades_contained_read_to_the_covering_write() {
1798 let mut reg = SegmentBorrowRegistry::new();
1799 let r = reg.register_leased_read(&key(1), 4, 4).unwrap();
1800 reg.release(&r);
1801 let mut i: u32 = 0;
1802 while (i as usize) < MAX_TOUCH_RECORDS - 1 {
1803 let b = reg.register_leased_read(&key(2), i * 16, 8).unwrap();
1804 reg.release(&b);
1805 i += 1;
1806 }
1807 assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
1808
1809 let w = reg.register_leased_write(&key(1), 0, 64).unwrap();
1810 reg.release(&w);
1811 assert!(!reg.touch_map_overflowed());
1812 assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
1813 let mut key1_records = std::vec::Vec::new();
1814 reg.for_each_touch(|t| {
1815 if t.key == key(1) {
1816 key1_records.push((t.offset, t.size, t.kind));
1817 }
1818 });
1819 assert_eq!(key1_records, [(0, 64, AccessKind::Write)]);
1820 }
1821
1822 /// When the incoming range cannot be absorbed anywhere, compaction
1823 /// folds mergeable neighbors to free a slot, and first-touch order
1824 /// survives (the merged record keeps its earliest constituent's
1825 /// slot; the newcomer appends after).
1826 #[test]
1827 fn pressure_compaction_reclaims_slots_from_mergeable_neighbors() {
1828 let mut reg = SegmentBorrowRegistry::new();
1829 // 32 pairwise-ADJACENT reads: below capacity they stay granular.
1830 let mut i: u32 = 0;
1831 while (i as usize) < MAX_TOUCH_RECORDS {
1832 let b = reg.register_leased_read(&key(5), i * 8, 8).unwrap();
1833 reg.release(&b);
1834 i += 1;
1835 }
1836 assert_eq!(reg.touch_map_len(), MAX_TOUCH_RECORDS);
1837 assert!(!reg.touch_map_overflowed());
1838
1839 // A range on another account absorbs nowhere; compaction folds
1840 // the adjacent reads into one exact-union record and the
1841 // newcomer takes a freed slot. No overflow.
1842 let b = reg.register_leased_write(&key(6), 0, 8).unwrap();
1843 reg.release(&b);
1844 assert!(!reg.touch_map_overflowed());
1845 assert_eq!(reg.touch_map_len(), 2);
1846 let mut seen = std::vec::Vec::new();
1847 reg.for_each_touch(|t| seen.push((t.key, t.offset, t.size, t.kind)));
1848 assert_eq!(
1849 seen[0],
1850 (key(5), 0, MAX_TOUCH_RECORDS as u32 * 8, AccessKind::Read)
1851 );
1852 assert_eq!(seen[1], (key(6), 0, 8, AccessKind::Write));
1853 }
1854
1855 /// The merge rule itself, pinned edge by edge: exact unions only.
1856 #[test]
1857 fn merge_exact_rules_are_exact_union_only() {
1858 use super::touch_log::merge_exact;
1859 let mk = |offset: u32, size: u32, kind: AccessKind| SegmentBorrow {
1860 key_fp: address_fingerprint(&key(1)),
1861 key: key(1),
1862 offset,
1863 size,
1864 kind,
1865 };
1866 // Same kind: adjacency and overlap merge to the exact union.
1867 let m = merge_exact(&mk(0, 8, AccessKind::Write), &mk(8, 8, AccessKind::Write)).unwrap();
1868 assert_eq!((m.offset, m.size, m.kind), (0, 16, AccessKind::Write));
1869 let m = merge_exact(&mk(4, 8, AccessKind::Read), &mk(0, 6, AccessKind::Read)).unwrap();
1870 assert_eq!((m.offset, m.size, m.kind), (0, 12, AccessKind::Read));
1871 // A gap never bridges, the union would claim untouched bytes.
1872 assert!(merge_exact(&mk(0, 8, AccessKind::Write), &mk(9, 8, AccessKind::Write)).is_none());
1873 // Different accounts never merge.
1874 let other = SegmentBorrow {
1875 key_fp: address_fingerprint(&key(2)),
1876 key: key(2),
1877 offset: 8,
1878 size: 8,
1879 kind: AccessKind::Write,
1880 };
1881 assert!(merge_exact(&mk(0, 8, AccessKind::Write), &other).is_none());
1882 // Cross-kind: a contained read is absorbed by the write,
1883 // unchanged, in either argument order...
1884 let m = merge_exact(&mk(4, 4, AccessKind::Read), &mk(0, 64, AccessKind::Write)).unwrap();
1885 assert_eq!((m.offset, m.size, m.kind), (0, 64, AccessKind::Write));
1886 let m = merge_exact(&mk(0, 64, AccessKind::Write), &mk(4, 4, AccessKind::Read)).unwrap();
1887 assert_eq!((m.offset, m.size, m.kind), (0, 64, AccessKind::Write));
1888 // ...but a read poking outside the write must NOT merge, the
1889 // union would fake write access to read-only bytes.
1890 assert!(merge_exact(&mk(60, 8, AccessKind::Read), &mk(0, 64, AccessKind::Write)).is_none());
1891 assert!(merge_exact(&mk(0, 64, AccessKind::Write), &mk(60, 8, AccessKind::Read)).is_none());
1892 // A same-kind union too large for u32 is refused, not truncated.
1893 assert!(merge_exact(
1894 &mk(0, u32::MAX, AccessKind::Write),
1895 &mk(u32::MAX - 1, 2, AccessKind::Write),
1896 )
1897 .is_none());
1898 }
1899
1900 #[test]
1901 fn touch_map_encoder_round_trips_including_flags() {
1902 let records = [
1903 TouchMapRecord {
1904 slot: 0,
1905 offset: 16,
1906 size: 8,
1907 write: true,
1908 },
1909 TouchMapRecord {
1910 slot: 3,
1911 offset: 0,
1912 size: 64,
1913 write: false,
1914 },
1915 TouchMapRecord {
1916 slot: 255,
1917 offset: 0x7FFF_FFFF,
1918 size: 1,
1919 write: true,
1920 },
1921 ];
1922 let (buf, len) = encode_touch_map(&records, false, false);
1923 assert_eq!(len, TOUCH_MAP_HEADER_LEN + 3 * TOUCH_MAP_RECORD_LEN);
1924 let (flags, decoded) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1925 assert_eq!(flags, 0);
1926 assert_eq!(decoded, records);
1927
1928 // Overflow flag survives the round trip.
1929 let (buf, len) = encode_touch_map(&records, true, false);
1930 let (flags, decoded) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1931 assert_eq!(flags, TOUCH_MAP_FLAG_OVERFLOWED);
1932 assert_eq!(decoded, records);
1933
1934 // Skipped flag survives the round trip.
1935 let (buf, len) = encode_touch_map(&records, false, true);
1936 let (flags, _) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1937 assert_eq!(flags, TOUCH_MAP_FLAG_SKIPPED);
1938 }
1939
1940 #[test]
1941 fn touch_map_encoder_skips_unrepresentable_offsets_honestly() {
1942 let records = [
1943 TouchMapRecord {
1944 slot: 0,
1945 offset: 8,
1946 size: 8,
1947 write: false,
1948 },
1949 TouchMapRecord {
1950 slot: 1,
1951 offset: 0x8000_0000, // does not fit in 31 bits
1952 size: 8,
1953 write: true,
1954 },
1955 ];
1956 let (buf, len) = encode_touch_map(&records, false, false);
1957 let (flags, decoded) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1958 assert_eq!(flags, TOUCH_MAP_FLAG_SKIPPED);
1959 assert_eq!(decoded.len(), 1);
1960 assert_eq!(decoded[0], records[0]);
1961 }
1962
1963 #[test]
1964 fn touch_map_encoder_never_lies_about_record_count() {
1965 // Feeding more records than the wire format can carry must mark
1966 // the map as overflowed, not overrun or misreport the count.
1967 let records = std::vec![
1968 TouchMapRecord {
1969 slot: 0,
1970 offset: 0,
1971 size: 1,
1972 write: false,
1973 };
1974 MAX_TOUCH_RECORDS + 2
1975 ];
1976 let (buf, len) = encode_touch_map(&records, false, false);
1977 assert_eq!(
1978 len,
1979 TOUCH_MAP_HEADER_LEN + MAX_TOUCH_RECORDS * TOUCH_MAP_RECORD_LEN
1980 );
1981 let (flags, decoded) = decode_touch_map_for_tests(&buf[..len]).unwrap();
1982 assert_eq!(flags & TOUCH_MAP_FLAG_OVERFLOWED, TOUCH_MAP_FLAG_OVERFLOWED);
1983 assert_eq!(decoded.len(), MAX_TOUCH_RECORDS);
1984 }
1985
1986 #[test]
1987 fn touch_map_decoder_rejects_wrong_magic_version_and_length() {
1988 let (buf, len) = encode_touch_map(
1989 &[TouchMapRecord {
1990 slot: 0,
1991 offset: 4,
1992 size: 4,
1993 write: true,
1994 }],
1995 false,
1996 false,
1997 );
1998 assert!(decode_touch_map_for_tests(&buf[..len]).is_some());
1999
2000 let mut bad_magic = buf;
2001 bad_magic[0] = 0x7B;
2002 assert!(decode_touch_map_for_tests(&bad_magic[..len]).is_none());
2003
2004 let mut bad_version = buf;
2005 bad_version[1] = 0x02;
2006 assert!(decode_touch_map_for_tests(&bad_version[..len]).is_none());
2007
2008 // Truncated and over-long payloads violate len == 4 + 9 * count.
2009 assert!(decode_touch_map_for_tests(&buf[..len - 1]).is_none());
2010 assert!(decode_touch_map_for_tests(&buf[..len + 9]).is_none());
2011 }
2012
2013 /// The SBF tier materializes [`touch_log::TouchLog`] over the VM's
2014 /// ZEROED heap with no initialization at all, so the all-zero byte
2015 /// pattern being the valid EMPTY log is load-bearing, same pin as
2016 /// `initial_gate_store_is_all_zero_bytes` for the gate store.
2017 /// Checked field-wise in both directions (zeroed-overlay reads
2018 /// empty; `new()` is field-for-field zero): a whole-struct byte
2019 /// view would read uninitialized PADDING bytes, UB the Miri Tree
2020 /// Borrows lane caught in the previous form of this test.
2021 #[cfg(feature = "touch-map")]
2022 #[test]
2023 fn initial_touch_log_is_all_zero_bytes() {
2024 let zeroed = std::vec![0u64; core::mem::size_of::<touch_log::TouchLog>().div_ceil(8)];
2025 touch_log::assert_all_zero_is_the_valid_empty_log(&zeroed);
2026 }
2027
2028 /// The gap this ambient move closes: a typed mutable load taken
2029 /// straight off an `AccountView`, NO `Context` anywhere, must
2030 /// land in the instruction touch log, because that is exactly what
2031 /// wrapper accessors (`Account::get_mut`) do under the hood.
2032 #[cfg(feature = "touch-map")]
2033 #[test]
2034 fn bare_account_view_load_mut_records_ambiently() {
2035 use crate::layout::{write_header, HopperHeader, LayoutContract};
2036 use hopper_native::{
2037 AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
2038 NOT_BORROWED,
2039 };
2040
2041 #[repr(C)]
2042 #[derive(Clone, Copy)]
2043 struct Blob {
2044 v: [u8; 8],
2045 }
2046 // SAFETY: repr(C), byte-array field, every bit pattern valid,
2047 // align 1, no padding.
2048 unsafe impl crate::Zeroable for Blob {}
2049 // SAFETY: as above.
2050 unsafe impl crate::Pod for Blob {}
2051 // SAFETY: test-local layout upholding the sealed overlay contract.
2052 unsafe impl crate::zerocopy::__sealed::HopperZeroCopySealed for Blob {}
2053 impl crate::field_map::FieldMap for Blob {
2054 const FIELDS: &'static [crate::field_map::FieldInfo] =
2055 &[crate::field_map::FieldInfo::new("v", HopperHeader::SIZE, 8)];
2056 }
2057 impl LayoutContract for Blob {
2058 const DISC: u8 = 55;
2059 const VERSION: u8 = 1;
2060 const LAYOUT_ID: [u8; 8] = [0x55; 8];
2061 const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
2062 }
2063
2064 const DATA_LEN: usize = HopperHeader::SIZE + 8;
2065 let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + DATA_LEN).div_ceil(8)];
2066 let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
2067 // SAFETY: backing is sized for the header plus DATA_LEN bytes
2068 // and outlives the view (this frame holds the Vec).
2069 unsafe {
2070 raw.write(RuntimeAccount {
2071 borrow_state: NOT_BORROWED,
2072 is_signer: 0,
2073 is_writable: 1,
2074 executable: 0,
2075 resize_delta: 0,
2076 address: NativeAddress::new_from_array([3; 32]),
2077 owner: NativeAddress::new_from_array([4; 32]),
2078 lamports: 1,
2079 data_len: DATA_LEN as u64,
2080 });
2081 }
2082 // SAFETY: raw points at a fully initialized RuntimeAccount.
2083 let backend = unsafe { NativeAccountView::new_unchecked(raw) };
2084 let account = crate::AccountView::from_backend(backend);
2085 {
2086 let mut data = account.try_borrow_mut().unwrap();
2087 write_header(
2088 &mut data,
2089 <Blob as LayoutContract>::DISC,
2090 <Blob as LayoutContract>::VERSION,
2091 &<Blob as LayoutContract>::LAYOUT_ID,
2092 )
2093 .unwrap();
2094 }
2095
2096 touch_log::reset();
2097 // Raw byte borrows (the fixture write above) do NOT record,
2098 // only typed mutable loads do.
2099 assert_eq!(touch_log::len(), 0, "raw try_borrow_mut must not record");
2100
2101 drop(account.load_mut::<Blob>().unwrap());
2102
2103 let mut seen = std::vec::Vec::new();
2104 touch_log::for_each(|t| seen.push((t.key, t.offset, t.size, t.kind)));
2105 assert_eq!(
2106 seen,
2107 std::vec![(*account.address(), 0, DATA_LEN as u32, AccessKind::Write)],
2108 "a Context-less typed load_mut must land in the ambient log"
2109 );
2110
2111 // And a fresh Context scopes the log to a new instruction.
2112 let pid = crate::address::Address::new([9u8; 32]);
2113 let accounts: [crate::AccountView<'_>; 0] = [];
2114 let _ctx = crate::context::Context::new(&pid, &accounts, &[]);
2115 assert_eq!(
2116 touch_log::len(),
2117 0,
2118 "Context::new must reset the ambient log"
2119 );
2120 }
2121}