Skip to main content

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}