Skip to main content

hopper_runtime/
write_policy.rs

1//! Declared write sets enforced at borrow acquisition.
2//!
3//! Sealevel's account model stops at one bit of write granularity: the
4//! transaction-level `writable` flag covers the *entire* account. This
5//! module extends that to **byte-range granularity**: an instruction
6//! declares the exact ranges it is allowed to write, and the
7//! [`Context`](crate::context::Context) rejects any write borrow outside
8//! the declared set at acquisition time, before a byte is exposed mutably.
9//!
10//! ## How it composes
11//!
12//! - `#[hopper::context(strict_writes)]` compiles the context's declared
13//!   `mut` / `mut(seg, ...)` attributes into a `static` [`WritePolicy`]
14//!   and installs it during `bind()`. The descriptors are constant data, and
15//!   the policy scans that slice at each write acquisition.
16//! - Every Context-mediated write path is gated: segment writes
17//!   (`segment_mut`, `segment_mut_const`, `segment_mut_typed`,
18//!   `split_segments_mut`), whole-account typed loads (`load_mut`), and
19//!   the raw escape hatches (`raw_mut`, `as_mut_ptr`).
20//! - A whole-account borrow claims `[0, data_len)`, so under a policy that
21//!   declares only field ranges, `load_mut` / `as_mut_ptr` are refused and
22//!   the handler must use the declared segment accessors. That is the
23//!   discipline the policy exists to enforce.
24//! - Paired with the `touch-map` feature, the declared set can be
25//!   compared against the *actual* footprint in tests: declared-vs-actual
26//!   write verification from the emitted touch records.
27//!
28//! ## The lamport dimension
29//!
30//! Byte ranges cover **data**; Sealevel writability also covers
31//! **lamport** mutation (close, top-up, transfer). A policy built with
32//! [`WritePolicy::with_lamports`] declares that second dimension: the
33//! listed account indices may have their lamports mutated; other
34//! account's lamport mutation is refused. Because lamport operations
35//! flow through `AccountView` (not `Context`), enforcement uses an
36//! instruction-scoped ambient gate ([`try_install_lamport_gate`])
37//! consulted by the runtime's lamport choke points: the
38//! `native_boundary` `try_set_lamports`/`close` funnel used by the
39//! runtime and `hopper-core` lifecycle helpers, and the
40//! validated CPI tiers' writable-meta construction (a writable CPI
41//! hand-off is unbounded delegation of both dimensions).
42//!
43//! The gate stores address **values** copied at install time rather than a
44//! pointer into the account slice. Checks compare the address of
45//! the live view being mutated against those values. A leaked guard
46//! (`mem::forget`) therefore leaves an observable *stale value policy*
47//! installed (fail-closed for unknown addresses) rather than any form
48//! of memory unsafety; see the gate section below for the full
49//! contract, including the loud fail-closed install errors
50//! (`0xD1__` page).
51//!
52//! ## Enforcement boundary
53//!
54//! The policy governs access **through `Context`** (data ranges) and
55//! through the runtime's `AccountView`/CPI surface (lamports). Generated
56//! `#[hopper::context]` code and the documented safe APIs use those paths.
57//! Direct substrate access (`hopper_native` calls such
58//! as `batch::transfer_lamports`, or `try_borrow_mut` on the raw
59//! backend view) and the `unsafe` unchecked CPI tier are outside the
60//! governed surface, exactly like the documented raw-pointer escape
61//! hatches; they are visible in review and lintable. The Sealevel
62//! `writable` flag is still enforced underneath in all cases.
63//!
64//! For the common no-CPI lamport move the escape hatch has a
65//! first-class **gated** alternative:
66//! [`crate::lamports::transfer_lamports`] (re-exported at the crate
67//! root and through `hopper::prelude`; `lamports(...)` contexts also
68//! expose it as a generated `ctx.transfer_lamports(..)` method) runs
69//! the substrate helper's exact arithmetic through the
70//! `native_boundary` funnel, checking **both** sides against the gate
71//! before any balance changes, so gated programs retain the declared lamport
72//! checks without paying for a System CPI.
73//!
74//! [`AccountView`]: crate::account::AccountView
75
76use crate::address::Address;
77use crate::error::ProgramError;
78use crate::ProgramResult;
79
80mod exact_cell_selector_sealed {
81    pub trait Sealed {}
82
83    impl Sealed for u8 {}
84    impl Sealed for u16 {}
85    impl Sealed for u32 {}
86}
87
88/// A selector whose runtime value and wire representation are guaranteed to
89/// agree with Hopper's exact-cell Effect ABI.
90///
91/// The trait is sealed: aliases of the real unsigned primitives inherit the
92/// implementation, while signed integers and user-defined lookalikes cannot
93/// opt themselves in. Macro-generated code also checks [`WIRE_SIZE`](Self::WIRE_SIZE)
94/// against the authored type spelling, catching primitive-shadowing aliases
95/// such as a local `type u16 = u8` before a manifest can publish the wrong
96/// decoder width.
97pub trait ExactCellSelector: exact_cell_selector_sealed::Sealed + Copy {
98    /// Canonical fixed width of the selector on the instruction wire.
99    const WIRE_SIZE: u16;
100
101    /// Convert to the compact value consumed by the write-policy gate.
102    fn to_u32(self) -> u32;
103}
104
105impl ExactCellSelector for u8 {
106    const WIRE_SIZE: u16 = 1;
107
108    #[inline(always)]
109    fn to_u32(self) -> u32 {
110        self as u32
111    }
112}
113
114impl ExactCellSelector for u16 {
115    const WIRE_SIZE: u16 = 2;
116
117    #[inline(always)]
118    fn to_u32(self) -> u32 {
119        self as u32
120    }
121}
122
123impl ExactCellSelector for u32 {
124    const WIRE_SIZE: u16 = 4;
125
126    #[inline(always)]
127    fn to_u32(self) -> u32 {
128        self
129    }
130}
131
132/// Error page for write-policy violations.
133///
134/// A rejected write on account index `i` surfaces as
135/// `ProgramError::Custom(0xD000 | i)`, mirroring the `0xC000 | i`
136/// convention used for declarative constraint failures. The account
137/// index in the low byte makes the offending account recoverable from
138/// the bare error code in logs and explorers.
139pub const WRITE_POLICY_VIOLATION_PAGE: u32 = 0xD0_00;
140
141/// Build the write-policy-violation error for an account index.
142#[inline(always)]
143pub const fn write_policy_violation(account_index: u8) -> ProgramError {
144    ProgramError::Custom(WRITE_POLICY_VIOLATION_PAGE | account_index as u32)
145}
146
147/// One allowed write range on one instruction account.
148///
149/// `account_index` is the position in the instruction's account list
150/// (the same index handed to [`Context::account`](crate::context::Context::account)).
151/// Offsets are absolute within the account data, including any layout
152/// header bytes, the same convention the segment access primitives use.
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154pub struct WriteRange {
155    /// Instruction account-list index this range applies to.
156    pub account_index: u8,
157    /// Absolute byte offset of the allowed range.
158    pub offset: u32,
159    /// Byte size of the allowed range. [`WriteRange::whole_account`]
160    /// uses `u32::MAX`, which contains any request on the account
161    /// (account data is capped far below 4 GiB).
162    pub size: u32,
163}
164
165/// A runtime-selected fixed-size cell inside a statically declared column.
166///
167/// The static [`WriteRange`] remains the conservative envelope used by
168/// tooling. This descriptor narrows that envelope for one invocation to
169/// `base_offset + args[argument_index] * stride .. + cell_size`.
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub struct ParametricWriteRange {
172    /// Instruction account-list index this column belongs to.
173    pub account_index: u8,
174    /// Absolute offset of element zero.
175    pub base_offset: u32,
176    /// Byte distance between consecutive elements.
177    pub stride: u32,
178    /// Writable bytes in the selected element.
179    pub cell_size: u32,
180    /// Number of cells in the column.
181    pub count: u32,
182    /// Index into the invocation's bound `u32` policy arguments.
183    pub argument_index: u8,
184    /// Stable instruction-argument name for manifests and diagnostics.
185    pub argument_name: &'static str,
186    /// Stable state segment/field name for manifests and diagnostics.
187    pub segment_name: &'static str,
188}
189
190impl ParametricWriteRange {
191    /// Build one parametric column rule.
192    #[inline(always)]
193    #[expect(
194        clippy::too_many_arguments,
195        reason = "the constructor mirrors the eight independent manifest selector fields"
196    )]
197    pub const fn new(
198        account_index: u8,
199        base_offset: u32,
200        stride: u32,
201        cell_size: u32,
202        count: u32,
203        argument_index: u8,
204        argument_name: &'static str,
205        segment_name: &'static str,
206    ) -> Self {
207        Self {
208            account_index,
209            base_offset,
210            stride,
211            cell_size,
212            count,
213            argument_index,
214            argument_name,
215            segment_name,
216        }
217    }
218
219    #[inline(always)]
220    fn envelope_overlaps(&self, offset: u32, size: u32) -> bool {
221        let envelope_start = self.base_offset as u64;
222        let envelope_end = envelope_start
223            + self.stride as u64 * self.count.saturating_sub(1) as u64
224            + self.cell_size as u64;
225        let request_start = offset as u64;
226        let request_end = request_start + size as u64;
227        request_start < envelope_end && request_end > envelope_start
228    }
229
230    #[inline(always)]
231    fn selected_contains(&self, selected: u32, offset: u32, size: u32) -> bool {
232        if selected >= self.count {
233            return false;
234        }
235        let start = self.base_offset as u64 + self.stride as u64 * selected as u64;
236        let end = start + self.cell_size as u64;
237        let request_start = offset as u64;
238        let request_end = request_start + size as u64;
239        request_start >= start && request_end <= end
240    }
241}
242
243impl WriteRange {
244    /// Allow writes to `[offset, offset + size)` on `account_index`.
245    #[inline(always)]
246    pub const fn new(account_index: u8, offset: u32, size: u32) -> Self {
247        Self {
248            account_index,
249            offset,
250            size,
251        }
252    }
253
254    /// Allow whole-account writes on `account_index` (what a plain
255    /// `mut` declaration grants).
256    #[inline(always)]
257    pub const fn whole_account(account_index: u8) -> Self {
258        Self {
259            account_index,
260            offset: 0,
261            size: u32::MAX,
262        }
263    }
264
265    /// Allow writes to the **open-ended tail** `[offset, +inf)` on
266    /// `account_index`, the grant a growable `Seq<T>` tail needs.
267    ///
268    /// `size` is `u32::MAX`, so [`contains`](Self::contains) admits any
269    /// sub-range starting at or after `offset` regardless of how large the
270    /// account grows (the account data cap is far below 4 GiB, and
271    /// `contains` widens to `u64` so `offset + u32::MAX` cannot wrap).
272    ///
273    /// Crucially this is **not** a whole-account grant when `offset != 0`:
274    /// [`allows_whole_account_write`](WritePolicy::allows_whole_account_write)
275    /// requires a range containing `[0, u32::MAX)`, and a tail range that
276    /// starts past the fixed head fails that test. So the fixed head stays
277    /// byte-protected and CPI writable-meta delegation stays refused, while
278    /// the tail region past `offset` remains freely writable and growable.
279    #[inline(always)]
280    pub const fn tail_from(account_index: u8, offset: u32) -> Self {
281        Self {
282            account_index,
283            offset,
284            size: u32::MAX,
285        }
286    }
287
288    /// Whether `[offset, offset + size)` is fully contained in this
289    /// range. Widened to `u64` so `offset + size` cannot wrap.
290    #[inline(always)]
291    pub const fn contains(&self, offset: u32, size: u32) -> bool {
292        let req_start = offset as u64;
293        let req_end = offset as u64 + size as u64;
294        let start = self.offset as u64;
295        let end = self.offset as u64 + self.size as u64;
296        req_start >= start && req_end <= end
297    }
298}
299
300/// The lamport-write dimension of a [`WritePolicy`].
301///
302/// A [`WriteRange`] covers **data** bytes; Sealevel writability also
303/// covers **lamport** credits/debits (close, top-up, transfer). This
304/// enum records whether the instruction declared that second dimension:
305///
306/// - [`Undeclared`](Self::Undeclared): the policy says nothing about
307///   lamports. Lamport mutation stays ungoverned (today's behavior for
308///   every pre-existing `strict_writes` context), and the write set is
309///   **not** mutation-complete.
310/// - [`Declared`](Self::Declared): only the listed account indices may
311///   have their lamports mutated through the runtime's lamport choke
312///   points; every other account's lamport mutation is refused with the
313///   same `Custom(0xD000 | index)` error the data-range gate uses. An
314///   **empty** list is valid and means "no lamport mutation anywhere".
315#[derive(Clone, Copy, Debug, PartialEq, Eq)]
316pub enum LamportPolicy {
317    /// Lamport writes undeclared: passthrough, not mutation-complete.
318    Undeclared,
319    /// Lamport writes permitted only on the listed account indices.
320    Declared(&'static [u8]),
321}
322
323/// Declared write-set for one instruction.
324///
325/// Intended to be a `static` built at macro-expansion time from the
326/// context's `mut` / `mut(seg, ...)` declarations and installed via
327/// [`Context::set_write_policy`](crate::context::Context::set_write_policy).
328/// An **empty** set is a valid policy: it denies every Context-mediated
329/// write, turning the instruction into a machine-checked read-only
330/// contract.
331///
332/// The optional [`lamports`](Self::lamports) dimension extends the set
333/// from data bytes to lamport balances; see [`LamportPolicy`].
334#[derive(Debug)]
335pub struct WritePolicy {
336    /// Allowed write ranges. Scanned linearly; contexts declare a
337    /// handful of ranges, so a bounded scan beats any lookup structure
338    /// at Solana scale.
339    pub allows: &'static [WriteRange],
340    /// Invocation-parametric rules that narrow selected static envelopes.
341    pub parametric: &'static [ParametricWriteRange],
342    /// Declared lamport-write permission set. See
343    /// [`LamportPolicy`] for the exact semantics of each variant.
344    pub lamports: LamportPolicy,
345}
346
347impl WritePolicy {
348    /// Wrap a const slice of allowed ranges as a policy.
349    ///
350    /// The lamport dimension is left [`LamportPolicy::Undeclared`], which
351    /// preserves the behavior without a mutation-completeness contract: lamport mutation is
352    /// ungoverned and the policy is not mutation-complete.
353    #[inline(always)]
354    pub const fn new(allows: &'static [WriteRange]) -> Self {
355        Self {
356            allows,
357            parametric: &[],
358            lamports: LamportPolicy::Undeclared,
359        }
360    }
361
362    /// Build a policy that declares **both** dimensions: data ranges and
363    /// the account indices allowed to have their lamports mutated.
364    #[inline(always)]
365    pub const fn with_lamports(
366        allows: &'static [WriteRange],
367        lamport_accounts: &'static [u8],
368    ) -> Self {
369        Self {
370            allows,
371            parametric: &[],
372            lamports: LamportPolicy::Declared(lamport_accounts),
373        }
374    }
375
376    /// Build a data policy with invocation-parametric cell narrowing.
377    #[inline(always)]
378    pub const fn with_parametric(
379        allows: &'static [WriteRange],
380        parametric: &'static [ParametricWriteRange],
381    ) -> Self {
382        Self {
383            allows,
384            parametric,
385            lamports: LamportPolicy::Undeclared,
386        }
387    }
388
389    /// Build a mutation-complete policy with parametric cell narrowing.
390    #[inline(always)]
391    pub const fn with_parametric_and_lamports(
392        allows: &'static [WriteRange],
393        parametric: &'static [ParametricWriteRange],
394        lamport_accounts: &'static [u8],
395    ) -> Self {
396        Self {
397            allows,
398            parametric,
399            lamports: LamportPolicy::Declared(lamport_accounts),
400        }
401    }
402
403    /// Whether the lamport dimension was declared (making the policy a
404    /// mutation-complete write-set when installed by a `strict_writes`
405    /// context).
406    #[inline(always)]
407    pub const fn lamports_declared(&self) -> bool {
408        matches!(self.lamports, LamportPolicy::Declared(_))
409    }
410
411    /// Whether the policy permits mutating `account_index`'s lamports.
412    ///
413    /// [`LamportPolicy::Undeclared`] permits everything (the dimension
414    /// carries no authority); a declared set permits only its members.
415    #[inline(always)]
416    pub fn allows_lamport_mutation(&self, account_index: u8) -> bool {
417        match self.lamports {
418            LamportPolicy::Undeclared => true,
419            LamportPolicy::Declared(indices) => {
420                let mut i = 0;
421                while i < indices.len() {
422                    if indices[i] == account_index {
423                        return true;
424                    }
425                    i += 1;
426                }
427                false
428            }
429        }
430    }
431
432    /// Whether a declared range grants **whole-account** data writes on
433    /// `account_index` (what a plain `mut` / lifecycle declaration
434    /// compiles to). Used by the CPI writable-meta gate: handing an
435    /// account writable to a callee is unbounded data delegation, so it
436    /// requires a whole-account grant, not just field ranges.
437    #[inline(always)]
438    pub fn allows_whole_account_write(&self, account_index: u8) -> bool {
439        let ranges = self.allows;
440        let mut i = 0;
441        while i < ranges.len() {
442            let r = &ranges[i];
443            if r.account_index == account_index && r.contains(0, u32::MAX) {
444                return true;
445            }
446            i += 1;
447        }
448        false
449    }
450
451    /// Whether this policy grants any data-write authority on
452    /// `account_index`.
453    ///
454    /// Length/presence transitions cannot be represented as an ordinary
455    /// byte range because the affected bytes may not exist yet.  The ambient
456    /// gate therefore treats a declared data range as the account-level
457    /// authority required to resize that account, while still requiring a
458    /// whole-account grant for raw full-buffer writes and writable CPI
459    /// delegation.
460    #[inline(always)]
461    pub fn allows_any_account_write(&self, account_index: u8) -> bool {
462        let mut i = 0;
463        while i < self.allows.len() {
464            if self.allows[i].account_index == account_index {
465                return true;
466            }
467            i += 1;
468        }
469        false
470    }
471
472    /// `Ok(())` iff `[offset, offset + size)` on `account_index` is
473    /// fully contained in a **single** declared range. Adjacent declared
474    /// ranges are not coalesced at check time; the macro emits ranges
475    /// exactly as declared, so a request straddling two declarations is
476    /// refused (declare a covering range if that access is intended).
477    #[inline(always)]
478    pub fn check_write(
479        &self,
480        account_index: u8,
481        offset: u32,
482        size: u32,
483    ) -> Result<(), ProgramError> {
484        let ranges = self.allows;
485        let mut i = 0;
486        while i < ranges.len() {
487            let r = &ranges[i];
488            if r.account_index == account_index && r.contains(offset, size) {
489                return Ok(());
490            }
491            i += 1;
492        }
493        Err(write_policy_violation(account_index))
494    }
495
496    /// Parametric form of [`check_write`](Self::check_write). A request that
497    /// touches a governed column must fit the invocation-selected cell; other
498    /// requests fall back to the ordinary static policy.
499    #[inline(always)]
500    pub fn check_write_with_args(
501        &self,
502        account_index: u8,
503        offset: u32,
504        size: u32,
505        args: &[u32],
506    ) -> Result<(), ProgramError> {
507        let mut i = 0;
508        while i < self.parametric.len() {
509            let rule = &self.parametric[i];
510            if rule.account_index == account_index && rule.envelope_overlaps(offset, size) {
511                let argument = args.get(rule.argument_index as usize).copied();
512                if argument
513                    .map(|selected| rule.selected_contains(selected, offset, size))
514                    .unwrap_or(false)
515                {
516                    return Ok(());
517                }
518                return Err(write_policy_violation(account_index));
519            }
520            i += 1;
521        }
522        self.check_write(account_index, offset, size)
523    }
524
525    /// Return the first byte in a recorded write touch that is not authorized
526    /// by this invocation's effective policy.
527    ///
528    /// Unlike [`check_write_with_args`](Self::check_write_with_args), this
529    /// method checks the **union** of authorized ranges. The distinction is
530    /// intentional: a write acquire must fit one declaration, but the touch
531    /// ledger may coalesce adjacent, independently authorized acquires into a
532    /// single record. Static ranges authorize bytes outside parametric
533    /// envelopes; inside an envelope, only the invocation-selected cell is
534    /// authorized. Missing or out-of-range selector values therefore fail
535    /// closed at the first governed byte.
536    #[inline]
537    pub fn first_unauthorized_byte_with_args(
538        &self,
539        account_index: u8,
540        offset: u32,
541        size: u32,
542        args: &[u32],
543    ) -> Option<u64> {
544        let mut cursor = offset as u64;
545        let request_end = cursor + size as u64;
546
547        while cursor < request_end {
548            // A parametric envelope replaces the static authority over its
549            // bytes. Rules generated by the context macro are disjoint; for
550            // hand-built overlapping rules, preserve the runtime gate's
551            // first-matching-rule behavior.
552            let mut governing_rule = None;
553            let mut i = 0;
554            while i < self.parametric.len() {
555                let rule = &self.parametric[i];
556                if rule.account_index == account_index {
557                    let envelope_start = rule.base_offset as u64;
558                    let envelope_end = envelope_start
559                        + rule.stride as u64 * rule.count.saturating_sub(1) as u64
560                        + rule.cell_size as u64;
561                    if envelope_start <= cursor && cursor < envelope_end {
562                        governing_rule = Some(rule);
563                        break;
564                    }
565                }
566                i += 1;
567            }
568
569            if let Some(rule) = governing_rule {
570                let Some(selected) = args.get(rule.argument_index as usize).copied() else {
571                    return Some(cursor);
572                };
573                if selected >= rule.count {
574                    return Some(cursor);
575                }
576                let selected_start = rule.base_offset as u64 + rule.stride as u64 * selected as u64;
577                let selected_end = selected_start + rule.cell_size as u64;
578                if selected_start <= cursor && cursor < selected_end {
579                    cursor = core::cmp::min(selected_end, request_end);
580                    continue;
581                }
582                return Some(cursor);
583            }
584
585            // Outside a governed envelope, advance through the union of all
586            // static ranges covering the cursor. Use the furthest end so the
587            // result is independent of declaration order.
588            let mut covered_until = cursor;
589            let mut j = 0;
590            while j < self.allows.len() {
591                let range = &self.allows[j];
592                if range.account_index == account_index {
593                    let range_start = range.offset as u64;
594                    let range_end = range_start + range.size as u64;
595                    if range_start <= cursor && cursor < range_end {
596                        covered_until = core::cmp::max(covered_until, range_end);
597                    }
598                }
599                j += 1;
600            }
601            if covered_until == cursor {
602                return Some(cursor);
603            }
604
605            // Do not jump across an envelope hidden inside a broad static
606            // range: its start is where parametric authority takes over.
607            let mut k = 0;
608            while k < self.parametric.len() {
609                let rule = &self.parametric[k];
610                let envelope_start = rule.base_offset as u64;
611                if rule.account_index == account_index
612                    && cursor < envelope_start
613                    && envelope_start < covered_until
614                {
615                    covered_until = envelope_start;
616                }
617                k += 1;
618            }
619            cursor = core::cmp::min(covered_until, request_end);
620        }
621
622        None
623    }
624
625    /// Non-erroring form of [`check_write`](Self::check_write).
626    #[inline(always)]
627    pub fn allows_write(&self, account_index: u8, offset: u32, size: u32) -> bool {
628        self.check_write(account_index, offset, size).is_ok()
629    }
630}
631
632// Lamport gate.
633//
634// The data dimension is enforced *inside* `Context`, which sees the
635// account index on every write acquire. Lamport mutation, by contrast,
636// happens through `AccountView`-level operations (`try_set_lamports`,
637// `close`, the CPI writable hand-off) that never see the `Context`.
638// The gate bridges that: `bind()` on a `strict_writes` context whose
639// policy declares the lamport dimension installs an instruction-scoped
640// ambient record derived from `(account slice, policy)`, and the
641// runtime's lamport choke points, `native_boundary::try_set_lamports`,
642// `native_boundary::close`, and the validated CPI tiers' writable-meta
643// construction, consult it before mutating.
644//
645// ## Value-based storage (nothing ambient is ever dereferenced)
646//
647// Storage mirrors `borrow_registry`: the ambient record holds address
648// VALUES copied out of the account slice at install time (plus the
649// per-address permission bits precomputed from the policy), never a
650// pointer into the slice. `check_*` compares the address the caller
651// reads from the live view it is about to mutate against those stored
652// values. Because nothing in the store is pointer-shaped, a leaked
653// guard (`mem::forget`, a forgotten bound context) degrades to a
654// **stale value policy**: the stale gate keeps governing later checks
655// on its tier, observable over-/stale enforcement that fails closed
656// for unknown addresses, until the tier's slots run out, at which
657// point further installs fail loudly. It can never become a dangling
658// read. (An earlier design stored a raw pointer to the account slice
659// and claimed the guard's lifetime bounded every ambient read; safe
660// `mem::forget` skips `Drop` and voided that claim, which is exactly
661// why values are stored instead.)
662//
663// Matching by address value means duplicate-meta accounts (same
664// address ⇒ same underlying account, as the loader hands duplicate
665// metas one `RuntimeAccount`) share one merged permission entry; the
666// permission is a property of the account, not of the meta position,
667// so an OR-merge across duplicate indices preserves the old
668// pointer-identity semantics. Each merged entry remembers the first
669// account index carrying the address so refusals keep the indexed
670// `Custom(0xD000 | index)` error shape.
671//
672// ## Slot store (no prev-chain)
673//
674// Each tier keeps a small fixed array of slots plus a monotonically
675// increasing token counter. Install claims a free slot under a unique
676// token; `Drop` clears exactly the slot holding its own token (scan +
677// match) and nothing else; checks consult the still-active slot with
678// the HIGHEST token, the most recently installed gate; so nested
679// binds shadow outer gates while alive and dropping guards in any
680// order can only ever free the dropper's own slot, never corrupt or
681// resurrect another gate. When no slot is free, install FAILS CLOSED
682// with a loud error instead of truncating, evicting, or silently
683// sharing.
684
685/// Error page for lamport-gate **installation** failures (`0xD1__`).
686///
687/// Deliberately distinct from the `0xD0__` per-account violation page so
688/// an install failure can never be mistaken for a policy refusal on a
689/// specific account index.
690pub const LAMPORT_GATE_INSTALL_ERROR_PAGE: u32 = 0xD1_00;
691
692/// Install refused: the instruction's account slice has more accounts
693/// than [`LAMPORT_GATE_CAPACITY`]. The gate refuses loudly rather than
694/// silently truncating the governed set (a truncated gate would treat
695/// the overflow accounts as foreign, surprising, and wrong the moment
696/// one of them was declared).
697pub const LAMPORT_GATE_TOO_MANY_ACCOUNTS: ProgramError =
698    ProgramError::Custom(LAMPORT_GATE_INSTALL_ERROR_PAGE | 0x01);
699
700/// Install refused: all [`LAMPORT_GATE_DEPTH`] slots on this tier are
701/// occupied by still-active (or leaked) gates. Nesting deeper than the
702/// Solana invoke-depth budget, or leaking guards via `mem::forget`,
703/// exhausts the store; the install fails closed instead of evicting a
704/// live gate.
705pub const LAMPORT_GATE_DEPTH_EXCEEDED: ProgramError =
706    ProgramError::Custom(LAMPORT_GATE_INSTALL_ERROR_PAGE | 0x02);
707
708/// Install refused (host fallback tier only): another still-active gate
709/// occupies the process-global single-slot store. On `no_std`
710/// multi-threaded hosts without the `thread-local-registry` feature the
711/// gate cannot attribute nesting to a thread, so a concurrent second
712/// install is refused loudly, never silently shared with, or allowed
713/// to corrupt, the gate another thread installed. Enable
714/// `thread-local-registry` (or run gated instructions one at a time)
715/// to lift this.
716pub const LAMPORT_GATE_CONTENDED: ProgramError =
717    ProgramError::Custom(LAMPORT_GATE_INSTALL_ERROR_PAGE | 0x03);
718
719/// Install refused because invocation-resolved exact-cell selectors exceed
720/// the bounded ambient ABI.
721pub const AMBIENT_GATE_TOO_MANY_ARGUMENTS: ProgramError =
722    ProgramError::Custom(LAMPORT_GATE_INSTALL_ERROR_PAGE | 0x04);
723
724/// Install refused: this build carries the `unguarded-raw-surfaces` size
725/// opt-out, but the policy declares data write ranges (fixed or
726/// parametric), governance the opt-out build cannot enforce on the raw
727/// `AccountView` surfaces. Refusing at install keeps the bypass loud on
728/// EVERY tier: macro-bound strict contexts are already a compile error in
729/// such builds, and this is the runtime fence for hand-rolled installs.
730/// Lamports-only policies still install (their dimensions stay enforced).
731pub const AMBIENT_GATE_UNGUARDED_BUILD: ProgramError =
732    ProgramError::Custom(LAMPORT_GATE_INSTALL_ERROR_PAGE | 0x05);
733
734/// Per-gate account capacity: the runtime's transaction account bound.
735/// An instruction can never carry more accounts than the transaction
736/// that contains it, so a gate over one instruction's slice always
737/// fits; anything larger is refused loudly at install
738/// ([`LAMPORT_GATE_TOO_MANY_ACCOUNTS`]).
739pub const LAMPORT_GATE_CAPACITY: usize = crate::MAX_TX_ACCOUNTS;
740
741/// Maximum number of invocation-resolved selector values retained by one
742/// ambient gate. This is the same bounded ABI used by [`Context`](crate::Context).
743pub const AMBIENT_GATE_ARG_CAPACITY: usize = 8;
744
745/// Concurrent gates per tier: Solana's nested-CPI depth budget (the
746/// runtime caps the instruction stack at 5 = one top-level + 4 nested
747/// CPI levels). On-chain every CPI level runs in a fresh VM whose
748/// writable data is re-initialized, so a single store only ever sees
749/// the nested binds of one handler frame; on host, hopper's CPI
750/// syscall is a no-op (no nested handler dispatch), so concurrent
751/// gates arise only from manually nested binds. Four slots cover both
752/// with room to spare, and exhaustion fails closed loudly
753/// ([`LAMPORT_GATE_DEPTH_EXCEEDED`]) rather than corrupting a live
754/// gate.
755pub const LAMPORT_GATE_DEPTH: usize = 4;
756
757/// Why a gate installation was refused (tier-independent). Mapped to a
758/// loud [`ProgramError`] by the install entry points; keeping the two
759/// cases distinct lets each tier name its own no-free-slot failure
760/// (nesting depth on the per-thread tiers vs. single-slot contention on
761/// the fallback tier).
762#[derive(Clone, Copy, Debug, PartialEq, Eq)]
763enum GateInstallError {
764    /// More accounts than [`LAMPORT_GATE_CAPACITY`].
765    TooManyAccounts,
766    /// More invocation selector values than
767    /// [`AMBIENT_GATE_ARG_CAPACITY`].
768    TooManyArguments,
769    /// Every slot on the tier is occupied.
770    NoFreeSlot,
771}
772
773/// One gated account: the address VALUE copied at install time plus the
774/// permission bits precomputed from the installed policy. Contains no
775/// pointers and is never dereferenced, only compared.
776#[derive(Clone, Copy)]
777struct GateEntry {
778    /// Address value copied from the account slice at install time.
779    address: Address,
780    /// First instruction account index carrying this address (for the
781    /// indexed `Custom(0xD000 | index)` refusal error).
782    index: u8,
783    /// OR over all indices with this address of
784    /// `policy.allows_lamport_mutation(index)`.
785    allow_mutation: bool,
786    /// OR over all indices with this address of `allows_lamport_mutation
787    /// && allows_whole_account_write` (the CPI writable hand-off needs
788    /// BOTH dimensions on one declared index).
789    allow_delegation: bool,
790    /// Whether the account has any declared data authority. Account-length
791    /// transitions are account-level effects whose newly exposed bytes do
792    /// not yet have an ordinary range, so this is the bounded transition
793    /// capability.
794    #[cfg_attr(feature = "unguarded-raw-surfaces", allow(dead_code))]
795    allow_transition: bool,
796}
797
798impl GateEntry {
799    #[cfg_attr(target_os = "solana", allow(dead_code))]
800    const EMPTY: Self = Self {
801        address: Address::new([0; 32]),
802        index: 0,
803        allow_mutation: false,
804        allow_delegation: false,
805        allow_transition: false,
806    };
807}
808
809/// One installed gate. `token == 0` means the slot is free.
810#[derive(Clone, Copy)]
811struct GateSlot {
812    /// Unique install token (0 = free slot).
813    token: u64,
814    /// Number of initialized entries (distinct addresses).
815    len: usize,
816    /// Static policy installed by generated code. The reference is safe even
817    /// if a guard is leaked: policies have program lifetime and never borrow
818    /// the instruction's account slice.
819    policy: Option<&'static WritePolicy>,
820    /// Invocation-decoded selectors used by parametric exact-cell rules.
821    args: [u32; AMBIENT_GATE_ARG_CAPACITY],
822    args_len: usize,
823    /// Copied `(address, permissions)` values; only `entries[..len]`
824    /// are meaningful.
825    entries: [GateEntry; LAMPORT_GATE_CAPACITY],
826}
827
828impl GateSlot {
829    #[cfg_attr(target_os = "solana", allow(dead_code))]
830    const FREE: Self = Self {
831        token: 0,
832        len: 0,
833        policy: None,
834        args: [0; AMBIENT_GATE_ARG_CAPACITY],
835        args_len: 0,
836        entries: [GateEntry::EMPTY; LAMPORT_GATE_CAPACITY],
837    };
838}
839
840#[derive(Clone, Copy)]
841enum GateCheck {
842    Lamports,
843    // Constructed only by the raw-surface guard wrappers; compiled out
844    // under the `unguarded-raw-surfaces` size opt-out so the variants do
845    // not read as dead code there.
846    #[cfg(not(feature = "unguarded-raw-surfaces"))]
847    Data {
848        offset: u32,
849        size: u32,
850    },
851    #[cfg(not(feature = "unguarded-raw-surfaces"))]
852    Transition,
853    Delegation,
854}
855
856/// Fixed-slot, value-only gate store. `DEPTH` is the number of
857/// concurrently installed gates the tier supports (see
858/// [`LAMPORT_GATE_DEPTH`]; the host fallback tier uses 1).
859struct GateStore<const DEPTH: usize> {
860    /// Count of tokens issued so far. The Nth install receives token `N`
861    /// (see [`GateStore::install`]), so `0` means "none issued yet" and
862    /// `0` remains the free-slot sentinel no live slot can hold.
863    ///
864    /// **This field must stay zero in [`GateStore::new`].** The SBF tier
865    /// holds this store in a `static mut`; an all-zero initializer lets
866    /// the linker place it in `.bss` (`NOBITS`, no file bytes), while a
867    /// *single* non-zero byte anywhere in the struct forces it into
868    /// `.data` (`PROGBITS`) and writes the whole
869    /// `DEPTH x capacity x size_of::<GateEntry>()` array, tens of KiB of
870    /// zeros, into every program's `.so`. That is exactly what a
871    /// `next_token: 1` initializer used to do. `initial_gate_store_is_all_zero_bytes`
872    /// pins this.
873    issued: u64,
874    /// Number of CURRENTLY installed (unremoved) gates, `0..=DEPTH`.
875    ///
876    /// This is the hot-path fast-out: `check_lamport_mutation` /
877    /// `check_lamport_delegation` run on EVERY runtime lamport write and
878    /// EVERY writable CPI meta, including in the vast majority of
879    /// programs that never declare `lamports(...)`. Without this counter
880    /// the no-gate path still walked the `DEPTH` slots per call, and the
881    /// router bench measured that dead scanning at ~+44 CU **per hop**
882    /// (2026-07-09: swap rows regressed 1564/3044/4525 → 1610/3136/4663,
883    /// flipping every row behind Quasar). `installed == 0` must make
884    /// `check` a single load + branch. Zero when zeroed (the all-zero
885    /// invariant below).
886    installed: u64,
887    /// Token of the CURRENT governing gate (highest live token), `0` when
888    /// none. Maintained on install (the fresh token is always the max)
889    /// and on remove (rescan only when the governing gate itself is
890    /// removed), so [`active_slot`](Self::active_slot) is O(1) instead of
891    /// a DEPTH-slot scan on every gated check. Zero when zeroed (the
892    /// all-zero invariant below).
893    top_token: u64,
894    /// Slot index holding [`top_token`](Self::top_token); meaningless
895    /// while `top_token == 0`. Zero when zeroed.
896    top_idx: u8,
897    slots: [GateSlot; DEPTH],
898}
899
900impl<const DEPTH: usize> GateStore<DEPTH> {
901    /// All-zero by construction; see [`GateStore::issued`]. Do not add a
902    /// non-zero field initializer here without re-reading that doc.
903    #[cfg_attr(target_os = "solana", allow(dead_code))]
904    const fn new() -> Self {
905        Self {
906            issued: 0,
907            installed: 0,
908            top_token: 0,
909            top_idx: 0,
910            slots: [GateSlot::FREE; DEPTH],
911        }
912    }
913
914    /// Copy `(address, permission)` values for every account into a free
915    /// slot and return that slot's unique token. Fails closed (no state
916    /// change) when the accounts outnumber the capacity or no slot is
917    /// free.
918    fn install_with_args(
919        &mut self,
920        accounts: &[crate::account::AccountView<'_>],
921        policy: &'static WritePolicy,
922        args: &[u32],
923    ) -> Result<u64, GateInstallError> {
924        if accounts.len() > LAMPORT_GATE_CAPACITY {
925            return Err(GateInstallError::TooManyAccounts);
926        }
927        if args.len() > AMBIENT_GATE_ARG_CAPACITY {
928            return Err(GateInstallError::TooManyArguments);
929        }
930        // NOTE (binary size): every index in this function is derived from a
931        // value LLVM cannot statically bound (a `usize::MAX` sentinel, or the
932        // stored `len`). A `slice[i]` it cannot prove in-bounds emits
933        // `panic_bounds_check`, which *formats* its arguments, dragging
934        // `core::fmt` (Formatter::pad_integral, do_count_chars, the integer
935        // Display impls) into `.text`. One such site costs ~5 KiB in every
936        // Hopper program. So this whole path uses iterators / `get`/`get_mut`,
937        // which are provably panic-free. Keep it that way.
938        let (slot_idx, slot) = self
939            .slots
940            .iter_mut()
941            .enumerate()
942            .find(|(_, s)| s.token == 0)
943            .ok_or(GateInstallError::NoFreeSlot)?;
944
945        let mut len = 0usize;
946        for (i, view) in accounts.iter().enumerate() {
947            // Reading `address()` here is an always-safe read of a live
948            // reference the caller just gave us; only the VALUE is kept.
949            let address = *view.address();
950            // Indices above u8::MAX can never have been declared (the
951            // policy wire format is u8): permissionless. Unreachable
952            // while LAMPORT_GATE_CAPACITY <= 255, kept for honesty.
953            let (index, allow_mutation, allow_delegation, allow_transition) =
954                if i <= u8::MAX as usize {
955                    let idx = i as u8;
956                    let m = policy.allows_lamport_mutation(idx);
957                    (
958                        idx,
959                        m,
960                        policy.lamports_declared() && m && policy.allows_whole_account_write(idx),
961                        policy.allows_any_account_write(idx),
962                    )
963                } else {
964                    (u8::MAX, false, false, false)
965                };
966            // Keep one entry per account position. Duplicate addresses are
967            // still OR-authorized by the check scan, while retaining every
968            // index lets exact byte ranges be evaluated correctly.
969            let Some(entry) = slot.entries.get_mut(len) else {
970                return Err(GateInstallError::TooManyAccounts);
971            };
972            *entry = GateEntry {
973                address,
974                index,
975                allow_mutation,
976                allow_delegation,
977                allow_transition,
978            };
979            len += 1;
980        }
981        slot.len = len;
982        slot.policy = Some(policy);
983        slot.args_len = args.len();
984        for (dst, src) in slot.args.iter_mut().zip(args.iter().copied()) {
985            *dst = src;
986        }
987        // Tokens are 1, 2, 3, ..., `issued` counts up from a zeroed store
988        // (which is what keeps this static in `.bss`; see the field doc).
989        // 0 is the free-slot sentinel, so skip it on the (2^64 installs,
990        // practically unreachable) wrap.
991        self.issued = self.issued.wrapping_add(1);
992        if self.issued == 0 {
993            self.issued = 1;
994        }
995        let token = self.issued;
996        slot.token = token;
997        // The fresh token is strictly the highest live token, so it is
998        // always the new governing gate.
999        self.top_token = token;
1000        self.top_idx = slot_idx as u8;
1001        self.installed += 1;
1002        Ok(token)
1003    }
1004
1005    /// Free exactly the slot installed under `token` (no-op when the
1006    /// token is not present; e.g. an inert guard). A guard can only
1007    /// ever clear its own slot, never another gate's. Returns whether a
1008    /// live slot was actually freed, so tier wrappers can maintain their
1009    /// fast-path liveness flag.
1010    fn remove(&mut self, token: u64) -> bool {
1011        if let Some(slot) = self.slots.iter_mut().find(|s| s.token == token) {
1012            slot.token = 0;
1013            slot.len = 0;
1014            slot.policy = None;
1015            slot.args_len = 0;
1016            // Saturating: a stale/forged token cannot reach here (the
1017            // find above gates on a live token), but keep the counter
1018            // incapable of wrapping regardless.
1019            self.installed = self.installed.saturating_sub(1);
1020            // Removing the governing gate resumes the next-highest live
1021            // one: rescan (rare path, only when the INNER of a nested
1022            // pair drops, never on ordinary single-gate teardown after
1023            // the counter already hit zero).
1024            if token == self.top_token {
1025                let mut best_token = 0u64;
1026                let mut best_idx = 0u8;
1027                for (i, s) in self.slots.iter().enumerate() {
1028                    if s.token > best_token {
1029                        best_token = s.token;
1030                        best_idx = i as u8;
1031                    }
1032                }
1033                self.top_token = best_token;
1034                self.top_idx = best_idx;
1035            }
1036            true
1037        } else {
1038            false
1039        }
1040    }
1041
1042    /// The governing gate: the still-active slot with the highest token,
1043    /// i.e. the most recently installed gate. Nested binds therefore
1044    /// shadow outer gates while alive; when the inner guard drops, the
1045    /// outer gate resumes governing.
1046    fn active_slot(&self) -> Option<&GateSlot> {
1047        // Tokens are unique and monotonic, so "highest token" is exactly
1048        // "most recently installed", maintained as `top_token`/`top_idx`
1049        // by install/remove, making this a two-load lookup instead of a
1050        // DEPTH-slot scan on every gated check. `get` (not indexing)
1051        // keeps the path provably panic-free (see the binary-size note in
1052        // `install_with_args`).
1053        if self.top_token == 0 {
1054            return None;
1055        }
1056        self.slots.get(self.top_idx as usize)
1057    }
1058
1059    /// Match `address` against the governing gate's stored values.
1060    /// Passthrough when no gate is installed; fail closed (indexed
1061    /// error, `u8::MAX` for addresses foreign to the gated instruction)
1062    /// otherwise.
1063    #[cfg_attr(feature = "unguarded-raw-surfaces", allow(unused_variables))]
1064    fn check(&self, address: &Address, check: GateCheck) -> ProgramResult {
1065        // Hot-path fast-out: programs that never declare `lamports(...)`
1066        // pay ONE load + branch here, not a slot walk. This check runs on
1067        // every lamport write and every writable CPI meta, so the walk
1068        // was measured at ~+44 CU per router hop before this guard.
1069        if self.installed == 0 {
1070            return Ok(());
1071        }
1072        let Some(slot) = self.active_slot() else {
1073            return Ok(());
1074        };
1075        let Some(policy) = slot.policy else {
1076            // A live token without its policy can only result from corrupted
1077            // ambient state. Refuse it as a foreign-account mutation.
1078            return Err(write_policy_violation(u8::MAX));
1079        };
1080        // A data-only strict policy (bare `strict_writes`, no
1081        // `lamports(...)`) intentionally leaves the LAMPORT dimension
1082        // ungoverned for backward compatibility: direct lamport
1083        // arithmetic AND writable-CPI delegation both pass through. Both
1084        // hand lamports to another party, delegation is a strict superset
1085        // of a direct debit; so a policy that carries no lamport
1086        // authority cannot govern either without retroactively refusing
1087        // lamport moves an already-deployed program performs. The DATA
1088        // dimension (raw data mutation, account transitions, out-of-set
1089        // accounts) stays fully governed. `mutation_complete` policies
1090        // declare `lamports(...)`, so neither carve-out fires for them and
1091        // delegation is refused as before.
1092        if !policy.lamports_declared()
1093            && matches!(check, GateCheck::Lamports | GateCheck::Delegation)
1094        {
1095            return Ok(());
1096        }
1097        // `get(..len)` rather than `entries[i]`: `len` is a stored field, so
1098        // LLVM cannot prove an index derived from it is in bounds and would
1099        // emit a formatting `panic_bounds_check` (~5 KiB of `core::fmt`).
1100        // A corrupt `len` degrades to an empty slice, i.e. fail closed.
1101        let seen = slot.entries.get(..slot.len).unwrap_or(&[]);
1102        let args = slot.args.get(..slot.args_len).unwrap_or(&[]);
1103        let mut first_matching_index = None;
1104        for entry in seen {
1105            if entry.address == *address {
1106                if first_matching_index.is_none() {
1107                    first_matching_index = Some(entry.index);
1108                }
1109                let allowed = match check {
1110                    GateCheck::Lamports => entry.allow_mutation,
1111                    #[cfg(not(feature = "unguarded-raw-surfaces"))]
1112                    GateCheck::Data { offset, size } => policy
1113                        .check_write_with_args(entry.index, offset, size, args)
1114                        .is_ok(),
1115                    #[cfg(not(feature = "unguarded-raw-surfaces"))]
1116                    GateCheck::Transition => entry.allow_transition,
1117                    GateCheck::Delegation => entry.allow_delegation,
1118                };
1119                if allowed {
1120                    return Ok(());
1121                }
1122            }
1123        }
1124        Err(write_policy_violation(
1125            first_matching_index.unwrap_or(u8::MAX),
1126        ))
1127    }
1128
1129    /// Whether any gate (governing or shadowed) is installed.
1130    fn any_active(&self) -> bool {
1131        self.installed != 0
1132    }
1133}
1134
1135/// Spinlocked single-slot gate store, the host fallback tier's cell
1136/// (`no_std` multi-threaded hosts without the `thread-local-registry`
1137/// feature). Also compiled under `test` so the tier's occupancy
1138/// semantics stay unit-testable while the thread-local tier is the
1139/// active one.
1140///
1141/// Single-slot occupancy is deliberate: one global store is shared by
1142/// every thread, so "nesting" cannot be attributed to a thread and a
1143/// second install while any gate is active is refused loudly
1144/// ([`LAMPORT_GATE_CONTENDED`]) instead of silently sharing or
1145/// corrupting another thread's gate. All address copies and
1146/// comparisons happen entirely inside [`Self::with_lock`]; nothing
1147/// pointer- or reference-shaped survives past the lock release.
1148#[cfg(all(
1149    not(target_os = "solana"),
1150    any(test, not(feature = "thread-local-registry"))
1151))]
1152struct SpinlockGateStore {
1153    lock: core::sync::atomic::AtomicBool,
1154    cell: core::cell::UnsafeCell<GateStore<1>>,
1155}
1156
1157// SAFETY: all access to `cell` goes through `with_lock`, which
1158// serializes via the acquire/release spinlock, so no two threads can
1159// observe the store concurrently; and the store contains only plain
1160// values (addresses, flags, tokens, no pointers or references), so no
1161// other thread-affine state is smuggled across threads.
1162#[cfg(all(
1163    not(target_os = "solana"),
1164    any(test, not(feature = "thread-local-registry"))
1165))]
1166unsafe impl Sync for SpinlockGateStore {}
1167
1168#[cfg(all(
1169    not(target_os = "solana"),
1170    any(test, not(feature = "thread-local-registry"))
1171))]
1172impl SpinlockGateStore {
1173    const fn new() -> Self {
1174        Self {
1175            lock: core::sync::atomic::AtomicBool::new(false),
1176            cell: core::cell::UnsafeCell::new(GateStore::new()),
1177        }
1178    }
1179
1180    fn with_lock<R>(&self, f: impl FnOnce(&mut GateStore<1>) -> R) -> R {
1181        use core::sync::atomic::Ordering;
1182        while self
1183            .lock
1184            .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
1185            .is_err()
1186        {
1187            core::hint::spin_loop();
1188        }
1189        // SAFETY: the spinlock above grants exclusive access to the
1190        // store until the release store below, and the store operations
1191        // passed as `f` (`install`/`remove`/`check`/`any_active`) never
1192        // call back into this module, so the `&mut` cannot be aliased
1193        // or re-entered while it is live.
1194        let result = f(unsafe { &mut *self.cell.get() });
1195        self.lock.store(false, Ordering::Release);
1196        result
1197    }
1198}
1199
1200/// Gate nesting depth on SBF. Each CPI level runs in its own VM with
1201/// its own heap, so cross-program nesting never shares this store;
1202/// depth is only consumed by one handler binding multiple
1203/// lamport-declared contexts simultaneously in the SAME instruction.
1204/// Two concurrent binds is already exotic; a third fails loudly with
1205/// [`LAMPORT_GATE_DEPTH_EXCEEDED`].
1206#[cfg(any(target_os = "solana", test))]
1207pub(crate) const SBF_GATE_DEPTH: usize = 2;
1208
1209/// Register the policy evaluator only when a gate is installed. Mutation
1210/// sites load this optional callback instead of referencing the evaluator
1211/// directly, so programs with no installation path can discard that code.
1212/// The callback is a program function, never a borrowed account pointer.
1213#[cfg(any(target_os = "solana", test))]
1214type GateChecker = fn(&GateStore<SBF_GATE_DEPTH>, &Address, GateCheck) -> ProgramResult;
1215
1216#[cfg(any(target_os = "solana", test))]
1217#[repr(C)]
1218struct SbfGateState {
1219    checker: Option<GateChecker>,
1220    store: GateStore<SBF_GATE_DEPTH>,
1221}
1222
1223#[cfg(any(target_os = "solana", test))]
1224impl SbfGateState {
1225    fn install_with_args(
1226        &mut self,
1227        accounts: &[crate::account::AccountView<'_>],
1228        policy: &'static WritePolicy,
1229        args: &[u32],
1230    ) -> Result<u64, GateInstallError> {
1231        let token = self.store.install_with_args(accounts, policy, args)?;
1232        self.checker = Some(GateStore::check);
1233        Ok(token)
1234    }
1235
1236    fn remove(&mut self, token: u64) {
1237        self.store.remove(token);
1238        if !self.store.any_active() {
1239            self.checker = None;
1240        }
1241    }
1242
1243    #[inline(always)]
1244    fn check(&self, address: &Address, check: GateCheck) -> ProgramResult {
1245        match self.checker {
1246            Some(checker) => checker(&self.store, address, check),
1247            None => Ok(()),
1248        }
1249    }
1250
1251    #[inline(always)]
1252    fn any_active(&self) -> bool {
1253        self.checker.is_some()
1254    }
1255}
1256
1257/// End offset (exclusive) of the lamport gate store inside the reserved
1258/// VM-heap scratch (`hopper_native::HEAP_RUNTIME_RESERVED`); i.e. the
1259/// first byte a LATER ambient consumer may claim. The touch log
1260/// (`segment_borrow::touch_log`) starts at this offset rounded up to 8.
1261/// Anyone adding a third consumer extends from THAT module's end the
1262/// same way, keeping the reserved-scratch layout a single linear chain.
1263#[cfg(target_os = "solana")]
1264#[allow(dead_code)]
1265pub(crate) const SBF_GATE_HEAP_END: usize =
1266    core::mem::size_of::<usize>() + core::mem::size_of::<SbfGateState>();
1267
1268#[cfg(target_os = "solana")]
1269mod gate_store {
1270    use super::{GateCheck, GateInstallError, SbfGateState, WritePolicy};
1271    use crate::address::Address;
1272    use crate::error::ProgramError;
1273    use crate::ProgramResult;
1274
1275    /// Byte offset of the gate store inside the VM heap region: right
1276    /// after the [`hopper_native::BumpAllocator`] cursor word.
1277    const GATE_HEAP_OFFSET: usize = core::mem::size_of::<usize>();
1278
1279    // The store must fit the reserved runtime scratch at the heap bottom,
1280    // and start 8-aligned (SbfGateState leads with an optional function).
1281    const _: () = assert!(
1282        core::mem::size_of::<SbfGateState>()
1283            <= hopper_native::HEAP_RUNTIME_RESERVED - GATE_HEAP_OFFSET,
1284        "GateStore exceeds HEAP_RUNTIME_RESERVED; grow the reservation in \
1285         hopper-native/src/entrypoint.rs or shrink the store"
1286    );
1287    const _: () = assert!((hopper_native::HEAP_START_ADDRESS + GATE_HEAP_OFFSET) % 8 == 0);
1288
1289    /// This tier supports two simultaneous same-instruction gates;
1290    /// running out means a handler bound three lamport-declared contexts
1291    /// at once (or leaked guards).
1292    pub(super) const NO_FREE_SLOT: ProgramError = super::LAMPORT_GATE_DEPTH_EXCEEDED;
1293
1294    /// Per-invocation gate store, living in the RESERVED BOTTOM of the VM
1295    /// heap (`[HEAP_START + 8, HEAP_START + 8 + size_of::<SbfGateState>...)`,
1296    /// see [`hopper_native::HEAP_RUNTIME_RESERVED`]).
1297    ///
1298    /// Why the heap and not a `static`: deployed SBF programs cannot carry
1299    /// writable sections AT ALL, the loader's ELF parser rejects
1300    /// `.bss`/`.data` (`WritableSectionNotSupported`), so ANY `static mut`
1301    /// here makes every program that links the gate fail to load. (Found
1302    /// empirically: Mollusk refused the parity vault; the loader is the
1303    /// same parser mainnet uses.) The VM heap is the one writable region a
1304    /// program owns, and it is **zero-initialized by the VM on every
1305    /// invocation**; which composes exactly with two invariants this
1306    /// module already pins:
1307    ///
1308    /// - `None` is the all-zero representation of `Option<fn>`, and
1309    ///   `GateStore::new()` has zero-valued fields, so zeroed heap is the
1310    ///   valid empty state; no init code runs at all.
1311    /// - Heap freshness per invocation gives instruction scoping for free:
1312    ///   state can never leak across transactions or CPI levels (each CPI
1313    ///   level is its own VM with its own heap).
1314    ///
1315    /// The `BumpAllocator`'s floor excludes this range, so allocations can
1316    /// never overwrite an installed gate.
1317    #[inline(always)]
1318    fn with_store<R>(f: impl FnOnce(&mut SbfGateState) -> R) -> R {
1319        let ptr = (hopper_native::HEAP_START_ADDRESS + GATE_HEAP_OFFSET) as *mut SbfGateState;
1320        // SAFETY: SBF execution is single-threaded and none of the store
1321        // operations passed as `f` (`install`/`remove`/`check`/
1322        // `any_active`) re-enter this module, so this exclusive reference
1323        // is unique for the duration of `f`. The pointee is valid: the VM
1324        // maps and ZEROES the heap region for every invocation, all-zero
1325        // bytes are a valid `SbfGateState` (the checker is None and the
1326        // store is empty), the address is 8-aligned
1327        // (const-asserted above) and the whole object lies inside
1328        // `HEAP_RUNTIME_RESERVED` (const-asserted above), a range the
1329        // `BumpAllocator` floor excludes and no other Hopper code touches.
1330        f(unsafe { &mut *ptr })
1331    }
1332
1333    pub(super) fn install_with_args(
1334        accounts: &[crate::account::AccountView<'_>],
1335        policy: &'static WritePolicy,
1336        args: &[u32],
1337    ) -> Result<u64, GateInstallError> {
1338        with_store(|store| store.install_with_args(accounts, policy, args))
1339    }
1340
1341    pub(super) fn remove(token: u64) {
1342        with_store(|store| store.remove(token));
1343    }
1344
1345    /// One callback load and branch when no gate is installed. Only the
1346    /// installation path references the full evaluator.
1347    #[inline(always)]
1348    pub(super) fn check(address: &Address, check: GateCheck) -> ProgramResult {
1349        with_store(|store| store.check(address, check))
1350    }
1351
1352    /// One heap load and compare; inlined at every guarded mutation site.
1353    #[inline(always)]
1354    pub(super) fn any_active() -> bool {
1355        with_store(|store| store.any_active())
1356    }
1357}
1358
1359#[cfg(all(
1360    not(target_os = "solana"),
1361    any(test, feature = "thread-local-registry")
1362))]
1363mod gate_store {
1364    use super::{GateCheck, GateInstallError, GateStore, WritePolicy, LAMPORT_GATE_DEPTH};
1365    use crate::address::Address;
1366    use crate::error::ProgramError;
1367    use crate::ProgramResult;
1368    use std::cell::RefCell;
1369
1370    // Per-thread gate store: hopper-runtime's own unit tests get it via
1371    // `test`; downstream test binaries opt in through the same
1372    // `thread-local-registry` feature the borrow registry uses, so
1373    // parallel test threads never observe each other's gates.
1374    std::thread_local! {
1375        static STORE: RefCell<GateStore<LAMPORT_GATE_DEPTH>> =
1376            const { RefCell::new(GateStore::new()) };
1377    }
1378
1379    /// This tier supports full nesting; running out of slots means
1380    /// nesting deeper than the Solana invoke-depth budget (or leaking
1381    /// guards).
1382    pub(super) const NO_FREE_SLOT: ProgramError = super::LAMPORT_GATE_DEPTH_EXCEEDED;
1383
1384    fn with_store<R>(f: impl FnOnce(&mut GateStore<LAMPORT_GATE_DEPTH>) -> R) -> R {
1385        STORE.with(|cell| f(&mut cell.borrow_mut()))
1386    }
1387
1388    pub(super) fn install_with_args(
1389        accounts: &[crate::account::AccountView<'_>],
1390        policy: &'static WritePolicy,
1391        args: &[u32],
1392    ) -> Result<u64, GateInstallError> {
1393        with_store(|store| store.install_with_args(accounts, policy, args))
1394    }
1395
1396    pub(super) fn remove(token: u64) {
1397        with_store(|store| store.remove(token));
1398    }
1399
1400    pub(super) fn check(address: &Address, check: GateCheck) -> ProgramResult {
1401        with_store(|store| store.check(address, check))
1402    }
1403
1404    pub(super) fn any_active() -> bool {
1405        with_store(|store| store.any_active())
1406    }
1407}
1408
1409#[cfg(all(
1410    not(target_os = "solana"),
1411    not(any(test, feature = "thread-local-registry"))
1412))]
1413mod gate_store {
1414    use super::{GateCheck, GateInstallError, SpinlockGateStore, WritePolicy};
1415    use crate::address::Address;
1416    use crate::error::ProgramError;
1417    use crate::ProgramResult;
1418
1419    /// Host fallback tier (`no_std` hosts without the thread-local
1420    /// feature): one process-global spinlocked slot. Cross-thread
1421    /// sharing means the one active gate governs every thread's lamport
1422    /// writes in this configuration, the same (documented) imprecision
1423    /// the fallback borrow registry accepts, and it fails closed rather
1424    /// than open. A second install while one gate is active is refused
1425    /// with [`super::LAMPORT_GATE_CONTENDED`] (see
1426    /// [`SpinlockGateStore`]); the per-thread and SBF tiers keep full
1427    /// nesting.
1428    static STORE: SpinlockGateStore = SpinlockGateStore::new();
1429
1430    /// Single-slot occupancy: a second concurrent install is contention,
1431    /// refused loudly instead of shared or corrupted.
1432    pub(super) const NO_FREE_SLOT: ProgramError = super::LAMPORT_GATE_CONTENDED;
1433
1434    pub(super) fn install_with_args(
1435        accounts: &[crate::account::AccountView<'_>],
1436        policy: &'static WritePolicy,
1437        args: &[u32],
1438    ) -> Result<u64, GateInstallError> {
1439        STORE.with_lock(|store| store.install_with_args(accounts, policy, args))
1440    }
1441
1442    pub(super) fn remove(token: u64) {
1443        STORE.with_lock(|store| store.remove(token));
1444    }
1445
1446    pub(super) fn check(address: &Address, check: GateCheck) -> ProgramResult {
1447        STORE.with_lock(|store| store.check(address, check))
1448    }
1449
1450    pub(super) fn any_active() -> bool {
1451        STORE.with_lock(|store| store.any_active())
1452    }
1453}
1454
1455/// RAII installation of the lamport gate for one bound instruction.
1456///
1457/// Returned by [`try_install_lamport_gate`] / [`install_lamport_gate`].
1458/// While this is the most recently installed still-active gate on its
1459/// tier, the runtime's lamport choke points refuse mutation on any
1460/// account the installed policy does not permit; an inner (newer) gate
1461/// shadows it until that inner guard drops. Dropping frees exactly this
1462/// guard's slot (matched by unique token), so guards may be dropped in
1463/// any order without disturbing, or resurrecting, other gates.
1464///
1465/// ## Leak behavior (`mem::forget`)
1466///
1467/// The gate store holds copied address values, never pointers into the
1468/// account slice, so leaking the guard leaves a **stale value policy**
1469/// installed: later checks on this tier keep being governed by it
1470/// (addresses it does not know fail closed) until enough leaks exhaust
1471/// the tier's [`LAMPORT_GATE_DEPTH`] slots and further installs fail
1472/// loudly. That is observable over-/stale enforcement, never memory
1473/// unsafety.
1474///
1475/// The `'accounts` lifetime parameter is retained for API stability
1476/// (macro codegen names `LamportGateGuard<'a>`); it is not load-bearing
1477/// for soundness, because nothing borrowed from the slice outlives the
1478/// install call.
1479#[derive(Debug)]
1480pub struct LamportGateGuard<'accounts> {
1481    /// Unique slot token; 0 = inert guard (nothing installed).
1482    token: u64,
1483    _accounts: core::marker::PhantomData<&'accounts ()>,
1484}
1485
1486impl Drop for LamportGateGuard<'_> {
1487    #[inline]
1488    fn drop(&mut self) {
1489        if self.token != 0 {
1490            gate_store::remove(self.token);
1491        }
1492    }
1493}
1494
1495/// Install the instruction-scoped lamport gate for `accounts` under
1496/// `policy`, the fallible entry point.
1497///
1498/// Returns an inert guard (nothing installed) unless `policy` declares
1499/// its lamport dimension ([`WritePolicy::with_lamports`]), a data-only
1500/// policy keeps the passthrough behavior. `#[hopper::context(
1501/// strict_writes, lamports(...))]`-generated `bind()` calls this (and
1502/// fails the bind on error) and stores the guard in the bound context,
1503/// so the gate lives exactly as long as the bound instruction scope.
1504///
1505/// # Errors
1506///
1507/// Fails closed, changing nothing, with
1508/// [`LAMPORT_GATE_TOO_MANY_ACCOUNTS`] (slice larger than
1509/// [`LAMPORT_GATE_CAPACITY`]), [`LAMPORT_GATE_DEPTH_EXCEEDED`] (all
1510/// [`LAMPORT_GATE_DEPTH`] slots on a nesting tier in use), or
1511/// [`LAMPORT_GATE_CONTENDED`] (host fallback tier: another still-active
1512/// gate occupies the process-global slot).
1513#[inline]
1514pub fn try_install_lamport_gate<'accounts>(
1515    accounts: &'accounts [crate::account::AccountView<'accounts>],
1516    policy: &'static WritePolicy,
1517) -> Result<LamportGateGuard<'accounts>, ProgramError> {
1518    try_install_ambient_gate_with_args(accounts, policy, &[])
1519}
1520
1521/// Install the complete instruction-scoped ambient mutation gate, including
1522/// invocation-resolved selector values for parametric exact-cell policies.
1523///
1524/// Generated `strict_writes` bindings use this entry point. A data-only
1525/// policy still preserves undeclared-lamport passthrough, but raw data
1526/// mutation, account transitions, writable CPI delegation, and accounts
1527/// outside the declared write-set are governed for the guard's lifetime.
1528///
1529/// Under the `unguarded-raw-surfaces` size opt-out, installing a policy
1530/// that declares data write ranges is refused with
1531/// [`AMBIENT_GATE_UNGUARDED_BUILD`], that build cannot enforce the raw
1532/// surfaces, and a silently half-enforced gate is worse than a loud
1533/// refusal. Lamports-only policies still install, with EXACTLY this
1534/// enforcement split (pinned by the opt-out-shape tests): the direct
1535/// lamport funnel (`check_lamport_mutation`) and writable-CPI
1536/// delegation (`check_lamport_delegation`) stay enforced; raw DATA
1537/// borrows and resize/close TRANSITIONS are compiled out with the rest
1538/// of the raw-surface guard, so a lamports-only gate on this build
1539/// refuses neither. A program that needs the data or transition
1540/// dimensions governed must not enable the opt-out.
1541#[inline]
1542pub fn try_install_ambient_gate_with_args<'accounts>(
1543    accounts: &'accounts [crate::account::AccountView<'accounts>],
1544    policy: &'static WritePolicy,
1545    args: &[u32],
1546) -> Result<LamportGateGuard<'accounts>, ProgramError> {
1547    // Runtime fence for the `unguarded-raw-surfaces` size opt-out: a
1548    // policy that declares data write ranges is asking for governance
1549    // this build cannot enforce on the raw `AccountView` surfaces, so
1550    // the install refuses loudly instead of degrading silently. The
1551    // macro tier can never reach this (strict contexts are a compile
1552    // error under the opt-out); this catches hand-rolled installs, and
1553    // costs nothing on default builds.
1554    #[cfg(feature = "unguarded-raw-surfaces")]
1555    if !policy.allows.is_empty() || !policy.parametric.is_empty() {
1556        return Err(AMBIENT_GATE_UNGUARDED_BUILD);
1557    }
1558    match gate_store::install_with_args(accounts, policy, args) {
1559        Ok(token) => Ok(LamportGateGuard {
1560            token,
1561            _accounts: core::marker::PhantomData,
1562        }),
1563        Err(GateInstallError::TooManyAccounts) => Err(LAMPORT_GATE_TOO_MANY_ACCOUNTS),
1564        Err(GateInstallError::TooManyArguments) => Err(AMBIENT_GATE_TOO_MANY_ARGUMENTS),
1565        Err(GateInstallError::NoFreeSlot) => Err(gate_store::NO_FREE_SLOT),
1566    }
1567}
1568
1569/// Infallible wrapper around [`try_install_lamport_gate`], kept for
1570/// call sites that predate the fallible API.
1571///
1572/// # Panics
1573///
1574/// Panics (failing the whole invocation, still fail-closed, still
1575/// loud) if the install is refused: more accounts than
1576/// [`LAMPORT_GATE_CAPACITY`], no free gate slot, or a contended host
1577/// fallback store. Callers that want the refusal as a
1578/// [`ProgramError`], every generated `bind()` does, must use
1579/// [`try_install_lamport_gate`].
1580#[inline]
1581pub fn install_lamport_gate<'accounts>(
1582    accounts: &'accounts [crate::account::AccountView<'accounts>],
1583    policy: &'static WritePolicy,
1584) -> LamportGateGuard<'accounts> {
1585    match try_install_lamport_gate(accounts, policy) {
1586        Ok(guard) => guard,
1587        Err(_) => panic!(
1588            "lamport gate install refused (capacity or slot occupancy); \
1589             use try_install_lamport_gate to handle this as a ProgramError"
1590        ),
1591    }
1592}
1593
1594/// Whether a lamport gate is currently installed on this tier
1595/// (diagnostic/testing). Counts shadowed and leaked (stale) gates.
1596#[inline]
1597pub fn lamport_gate_active() -> bool {
1598    gate_store::any_active()
1599}
1600
1601/// Gate a direct lamport mutation (`try_set_lamports` / `close`) on the
1602/// account with `address`.
1603///
1604/// The caller passes the address it read from the **live view** it is
1605/// about to mutate (an always-safe read); it is compared against the
1606/// address values copied at install time; nothing stored is
1607/// dereferenced. `Ok(())` when no gate is installed (the dimension is
1608/// opt-in) or when the governing gate permits lamport mutation on that
1609/// address.
1610#[inline(always)]
1611pub(crate) fn check_lamport_mutation(address: &Address) -> ProgramResult {
1612    // Test before constructing GateCheck so the no-policy path does not
1613    // spill its payload. On SBF this reads the optional checker word.
1614    if !gate_store::any_active() {
1615        return Ok(());
1616    }
1617    gate_store::check(address, GateCheck::Lamports)
1618}
1619
1620/// Gate a mutable data borrow against the invocation-resolved byte policy.
1621/// AccountView-level raw borrows pass the full buffer; segment paths pass the
1622/// exact range, so parametric cells remain enforceable outside `Context`.
1623#[inline(always)]
1624pub(crate) fn check_data_mutation(address: &Address, offset: u32, size: u32) -> ProgramResult {
1625    #[cfg(not(feature = "unguarded-raw-surfaces"))]
1626    {
1627        if !gate_store::any_active() {
1628            return Ok(());
1629        }
1630        gate_store::check(address, GateCheck::Data { offset, size })
1631    }
1632    #[cfg(feature = "unguarded-raw-surfaces")]
1633    {
1634        // Raw-tier opt-out: the raw-surface guard is compiled out, which
1635        // also unlinks the gate-check machinery from programs that never
1636        // install a gate. `strict_writes` codegen const-asserts
1637        // [`RAW_SURFACES_GUARDED`], so this branch cannot coexist with a
1638        // bound strict context.
1639        let _ = (address, offset, size);
1640        Ok(())
1641    }
1642}
1643
1644/// Gate a data-length/presence transition on an account. Any declared data
1645/// authority grants the transition capability; foreign and remaining-only
1646/// accounts fail closed.
1647#[inline(always)]
1648pub(crate) fn check_account_transition(address: &Address) -> ProgramResult {
1649    #[cfg(not(feature = "unguarded-raw-surfaces"))]
1650    {
1651        if !gate_store::any_active() {
1652            return Ok(());
1653        }
1654        gate_store::check(address, GateCheck::Transition)
1655    }
1656    #[cfg(feature = "unguarded-raw-surfaces")]
1657    {
1658        let _ = address;
1659        Ok(())
1660    }
1661}
1662
1663/// Whether the public raw `AccountView` surfaces are governed by the
1664/// ambient write gate in this build (the default). `false` only under the
1665/// `unguarded-raw-surfaces` opt-out; `strict_writes` codegen const-asserts
1666/// this is `true`, making the opt-out impossible to combine with a bound
1667/// strict context.
1668pub const RAW_SURFACES_GUARDED: bool = !cfg!(feature = "unguarded-raw-surfaces");
1669
1670/// Gate a CPI **writable meta** on the account with `address`. A
1671/// writable hand-off delegates unbounded data AND lamport mutation to
1672/// the callee, so it requires the account to carry both a
1673/// whole-account data grant and lamport permission. Same value-compare
1674/// contract as [`check_lamport_mutation`].
1675#[inline]
1676pub(crate) fn check_lamport_delegation(address: &Address) -> ProgramResult {
1677    gate_store::check(address, GateCheck::Delegation)
1678}
1679
1680// ── Tests ────────────────────────────────────────────────────────────
1681
1682#[cfg(test)]
1683mod gate_store_layout_tests {
1684    use super::*;
1685
1686    #[test]
1687    fn zeroed_sbf_state_has_no_evaluator() {
1688        // SAFETY: every GateStore field admits zero, including None for
1689        // optional policy references. Option<fn> guarantees a zero None.
1690        let state: SbfGateState = unsafe { core::mem::zeroed() };
1691        assert!(!state.any_active());
1692        assert_eq!(state.store.installed, 0);
1693        assert!(state
1694            .check(&Address::default(), GateCheck::Lamports)
1695            .is_ok());
1696        assert_eq!(core::mem::offset_of!(SbfGateState, checker), 0);
1697        assert_eq!(core::mem::size_of::<Option<GateChecker>>(), 8);
1698        assert!(
1699            core::mem::size_of::<SbfGateState>() + core::mem::size_of::<usize>()
1700                <= hopper_native::HEAP_RUNTIME_RESERVED
1701        );
1702    }
1703
1704    /// The SBF tier holds [`GateStore`] in a `static mut`. The linker puts
1705    /// it in `.bss` (`NOBITS`, **zero bytes in the `.so`**) only if its
1706    /// initializer is entirely zero; one non-zero byte moves it to `.data`
1707    /// (`PROGBITS`) and writes the whole ~35 KiB array of zeros into every
1708    /// Hopper program's binary. A `next_token: 1` initializer did exactly
1709    /// that, costing 35,656 file bytes, 61% of the vault's `.so`.
1710    ///
1711    /// This pins every field's initializer at zero. If you add a field to
1712    /// `GateStore`/`GateSlot`/`GateEntry`, its zero value must be valid, or
1713    /// the binary silently regrows.
1714    #[test]
1715    fn initial_gate_store_is_all_zero_bytes() {
1716        let store = GateStore::<2>::new();
1717        assert_eq!(store.issued, 0, "issued must start at 0, not 1");
1718        assert_eq!(store.installed, 0, "installed count must start at 0");
1719        for slot in &store.slots {
1720            assert_eq!(slot.token, 0, "free-slot sentinel is 0");
1721            assert_eq!(slot.len, 0);
1722            for entry in slot.entries.iter() {
1723                assert_eq!(*entry.address.as_array(), [0u8; 32]);
1724                assert_eq!(entry.index, 0);
1725                assert!(!entry.allow_mutation);
1726                assert!(!entry.allow_delegation);
1727            }
1728        }
1729    }
1730
1731    /// Tokens must still be non-zero and monotonic after the zero-init
1732    /// change (0 stays the free-slot sentinel), so nesting/shadowing and
1733    /// out-of-order drops keep working.
1734    #[test]
1735    fn issued_counter_hands_out_nonzero_monotonic_tokens() {
1736        let mut store = GateStore::<2>::new();
1737        store.issued = 0;
1738        store.issued = store.issued.wrapping_add(1);
1739        assert_eq!(store.issued, 1, "first token is 1, never the 0 sentinel");
1740        // Wrap: 2^64 installs must skip the sentinel rather than hand out 0.
1741        store.issued = u64::MAX;
1742        store.issued = store.issued.wrapping_add(1);
1743        if store.issued == 0 {
1744            store.issued = 1;
1745        }
1746        assert_eq!(store.issued, 1, "wrap skips the 0 sentinel");
1747    }
1748}
1749
1750#[cfg(test)]
1751mod tests {
1752    use super::*;
1753
1754    #[test]
1755    fn sbf_dispatch_preserves_nested_and_failed_install_enforcement() {
1756        let (_b0, a0) = make_account(70);
1757        let (_bf, foreign) = make_account(71);
1758        let accounts = [a0];
1759        static ALLOW: WritePolicy =
1760            WritePolicy::with_lamports(&[WriteRange::whole_account(0)], &[0]);
1761        static DENY: WritePolicy = WritePolicy::with_lamports(&[], &[]);
1762        let mut state = SbfGateState {
1763            checker: None,
1764            store: GateStore::new(),
1765        };
1766
1767        let outer = state.install_with_args(&accounts, &ALLOW, &[]).unwrap();
1768        assert!(state.any_active());
1769        assert!(state
1770            .check(accounts[0].address(), GateCheck::Delegation)
1771            .is_ok());
1772        assert_eq!(
1773            state.check(foreign.address(), GateCheck::Lamports),
1774            Err(write_policy_violation(u8::MAX))
1775        );
1776        let inner = state.install_with_args(&accounts, &DENY, &[]).unwrap();
1777        assert!(matches!(
1778            state.install_with_args(&accounts, &ALLOW, &[]),
1779            Err(GateInstallError::NoFreeSlot)
1780        ));
1781        assert_eq!(
1782            state.check(accounts[0].address(), GateCheck::Lamports),
1783            Err(write_policy_violation(0))
1784        );
1785        state.remove(inner);
1786        assert!(state
1787            .check(accounts[0].address(), GateCheck::Lamports)
1788            .is_ok());
1789        let inner = state.install_with_args(&accounts, &DENY, &[]).unwrap();
1790        state.remove(outer);
1791        state.remove(outer); // a stale guard must not clear the inner gate
1792        assert!(state.any_active());
1793        assert_eq!(
1794            state.check(accounts[0].address(), GateCheck::Delegation),
1795            Err(write_policy_violation(0))
1796        );
1797        state.remove(inner);
1798        assert!(!state.any_active());
1799        assert!(state.check(foreign.address(), GateCheck::Lamports).is_ok());
1800    }
1801
1802    #[test]
1803    #[cfg(not(feature = "unguarded-raw-surfaces"))]
1804    fn sbf_dispatch_preserves_byte_ranges_and_data_only_lamport_passthrough() {
1805        let (_b0, a0) = make_account(72);
1806        let (_bf, foreign) = make_account(73);
1807        let accounts = [a0];
1808        static NARROW: WritePolicy = WritePolicy::new(&[WriteRange::new(0, 8, 8)]);
1809        let mut state = SbfGateState {
1810            checker: None,
1811            store: GateStore::new(),
1812        };
1813        let token = state.install_with_args(&accounts, &NARROW, &[]).unwrap();
1814        assert!(state
1815            .check(
1816                accounts[0].address(),
1817                GateCheck::Data { offset: 8, size: 8 }
1818            )
1819            .is_ok());
1820        assert_eq!(
1821            state.check(
1822                accounts[0].address(),
1823                GateCheck::Data { offset: 9, size: 8 }
1824            ),
1825            Err(write_policy_violation(0))
1826        );
1827        assert_eq!(
1828            state.check(foreign.address(), GateCheck::Transition),
1829            Err(write_policy_violation(u8::MAX))
1830        );
1831        assert!(state.check(foreign.address(), GateCheck::Lamports).is_ok());
1832        assert!(state
1833            .check(foreign.address(), GateCheck::Delegation)
1834            .is_ok());
1835        state.remove(token);
1836        assert!(state
1837            .check(
1838                foreign.address(),
1839                GateCheck::Data {
1840                    offset: 0,
1841                    size: 32
1842                }
1843            )
1844            .is_ok());
1845    }
1846
1847    // vault (account 1): balance [16, 24), nonce [24, 32)
1848    static POLICY: WritePolicy = WritePolicy::new(&[
1849        WriteRange::new(1, 16, 8),
1850        WriteRange::new(1, 24, 8),
1851        WriteRange::whole_account(2),
1852    ]);
1853
1854    #[test]
1855    fn declared_ranges_allow_exact_and_contained_writes() {
1856        assert!(POLICY.check_write(1, 16, 8).is_ok());
1857        assert!(POLICY.check_write(1, 24, 8).is_ok());
1858        // Strictly inside a declared range is also allowed.
1859        assert!(POLICY.check_write(1, 18, 4).is_ok());
1860        // Zero-size request inside a range is trivially contained.
1861        assert!(POLICY.check_write(1, 20, 0).is_ok());
1862    }
1863
1864    #[test]
1865    fn parametric_column_allows_only_the_selected_cell() {
1866        static PARAMETRIC: &[ParametricWriteRange] = &[ParametricWriteRange::new(
1867            1, 100, 8, 8, 20, 0, "slot", "balances",
1868        )];
1869        static P: WritePolicy =
1870            WritePolicy::with_parametric(&[WriteRange::new(1, 100, 20 * 8)], PARAMETRIC);
1871
1872        assert!(P.check_write_with_args(1, 100 + 7 * 8, 8, &[7]).is_ok());
1873        assert!(P.check_write_with_args(1, 100 + 7 * 8 + 2, 4, &[7]).is_ok());
1874        assert_eq!(
1875            P.check_write_with_args(1, 100 + 8 * 8, 8, &[7]),
1876            Err(ProgramError::Custom(0xD0_01)),
1877        );
1878        assert!(P.check_write_with_args(1, 100 + 19 * 8, 8, &[20]).is_err());
1879        assert!(P.check_write_with_args(1, 100 + 7 * 8, 8, &[]).is_err());
1880    }
1881
1882    #[test]
1883    fn containment_oracle_resolves_parametric_cells_and_static_unions() {
1884        static PARAMETRIC: &[ParametricWriteRange] = &[ParametricWriteRange::new(
1885            1, 100, 8, 8, 20, 0, "slot", "balances",
1886        )];
1887        static P: WritePolicy = WritePolicy::with_parametric(
1888            &[
1889                WriteRange::new(1, 96, 4),
1890                WriteRange::new(1, 100, 20 * 8),
1891                WriteRange::new(1, 260, 4),
1892            ],
1893            PARAMETRIC,
1894        );
1895
1896        assert_eq!(
1897            P.first_unauthorized_byte_with_args(1, 100 + 7 * 8, 8, &[7]),
1898            None
1899        );
1900        assert_eq!(
1901            P.first_unauthorized_byte_with_args(1, 100 + 8 * 8, 8, &[7]),
1902            Some((100 + 8 * 8) as u64)
1903        );
1904        // Coalesced records may span an adjacent static acquire and the
1905        // selected first cell even though one runtime acquire may not.
1906        assert_eq!(P.first_unauthorized_byte_with_args(1, 96, 12, &[0]), None);
1907        assert_eq!(
1908            P.first_unauthorized_byte_with_args(1, 100, 8, &[]),
1909            Some(100)
1910        );
1911        assert_eq!(
1912            P.first_unauthorized_byte_with_args(1, 100, 8, &[20]),
1913            Some(100)
1914        );
1915
1916        // The non-parametric oracle walks the union, so a coalesced touch
1917        // over adjacent declarations remains certifiable.
1918        assert_eq!(
1919            POLICY.first_unauthorized_byte_with_args(1, 16, 16, &[]),
1920            None
1921        );
1922        assert_eq!(
1923            POLICY.first_unauthorized_byte_with_args(1, 15, 17, &[]),
1924            Some(15)
1925        );
1926    }
1927
1928    #[test]
1929    fn undeclared_ranges_are_refused_with_indexed_error() {
1930        // Outside every declared range.
1931        assert_eq!(
1932            POLICY.check_write(1, 0, 8),
1933            Err(ProgramError::Custom(0xD0_01))
1934        );
1935        // Overlapping but not contained.
1936        assert!(POLICY.check_write(1, 12, 8).is_err());
1937        // Straddling two adjacent declared ranges is refused: containment
1938        // is per-declaration, not per-union.
1939        assert!(POLICY.check_write(1, 16, 16).is_err());
1940        // Right account, range declared on a different account.
1941        assert_eq!(
1942            POLICY.check_write(0, 16, 8),
1943            Err(ProgramError::Custom(0xD0_00))
1944        );
1945    }
1946
1947    #[test]
1948    fn whole_account_allowance_contains_any_request() {
1949        assert!(POLICY.check_write(2, 0, 8).is_ok());
1950        assert!(POLICY.check_write(2, 0, u32::MAX).is_ok());
1951        assert!(POLICY.check_write(2, 4096, 10 * 1024 * 1024).is_ok());
1952    }
1953
1954    #[test]
1955    fn empty_policy_denies_all_writes() {
1956        static READ_ONLY: WritePolicy = WritePolicy::new(&[]);
1957        assert!(READ_ONLY.check_write(0, 0, 1).is_err());
1958        assert!(READ_ONLY.check_write(255, 0, 0).is_err());
1959    }
1960
1961    #[test]
1962    fn open_ended_tail_range_allows_tail_refuses_head_and_is_not_whole_account() {
1963        // A `Seq<T>` tail on account 1 whose fixed head occupies `[0, 24)`
1964        // (16-byte Hopper header + an 8-byte head field): the tail region
1965        // starts at offset 24 and is open-ended.
1966        const TAIL_OFF: u32 = 24;
1967        static P: WritePolicy = WritePolicy::new(&[WriteRange::tail_from(1, TAIL_OFF)]);
1968
1969        // Any sub-range at or past the tail offset is admitted, no matter
1970        // how large the account grows, the size is u32::MAX and `contains`
1971        // widens to u64 so `TAIL_OFF + u32::MAX` cannot wrap.
1972        assert!(P.check_write(1, TAIL_OFF, 4).is_ok()); // the u32 count prefix
1973        assert!(P.check_write(1, TAIL_OFF + 4, 32).is_ok()); // first element
1974        assert!(P.check_write(1, TAIL_OFF, 10 * 1024 * 1024).is_ok()); // grown far
1975        assert!(P.check_write(1, TAIL_OFF + 1_000_000, 32).is_ok());
1976
1977        // Every byte of the fixed head is refused with the indexed error,
1978        // the open-ended tail range does NOT leak backwards onto the head.
1979        assert_eq!(P.check_write(1, 0, 8), Err(write_policy_violation(1)));
1980        assert_eq!(P.check_write(1, 16, 8), Err(write_policy_violation(1)));
1981        // A write straddling the head/tail boundary is refused (it is not
1982        // fully contained in the tail range).
1983        assert!(P.check_write(1, TAIL_OFF - 1, 8).is_err());
1984
1985        // The load-bearing property: a tail range with `offset != 0` is
1986        // NOT a whole-account grant. CPI writable-meta delegation demands
1987        // `contains(0, u32::MAX)`, which starts at 0 and this range does
1988        // not; so delegation stays refused and the head stays protected.
1989        assert!(!P.allows_whole_account_write(1));
1990        // A tail range anchored at 0 (a degenerate "whole tail from the
1991        // start") IS a whole-account grant, by the same rule.
1992        static P0: WritePolicy = WritePolicy::new(&[WriteRange::tail_from(1, 0)]);
1993        assert!(P0.allows_whole_account_write(1));
1994    }
1995
1996    #[test]
1997    fn containment_survives_u32_boundary_arithmetic() {
1998        static EDGE: WritePolicy = WritePolicy::new(&[WriteRange::new(0, u32::MAX - 8, 8)]);
1999        // `offset + size` at the top of u32 must not wrap into a false allow.
2000        assert!(EDGE.check_write(0, u32::MAX - 8, 8).is_ok());
2001        assert!(EDGE.check_write(0, u32::MAX - 4, 8).is_err());
2002    }
2003
2004    // Lamport dimension.
2005
2006    #[test]
2007    fn undeclared_lamport_dimension_permits_everything_and_is_incomplete() {
2008        assert!(!POLICY.lamports_declared());
2009        assert!(POLICY.allows_lamport_mutation(0));
2010        assert!(POLICY.allows_lamport_mutation(255));
2011    }
2012
2013    #[test]
2014    fn declared_lamport_dimension_permits_only_members() {
2015        static P: WritePolicy =
2016            WritePolicy::with_lamports(&[WriteRange::whole_account(0)], &[0, 3]);
2017        assert!(P.lamports_declared());
2018        assert!(P.allows_lamport_mutation(0));
2019        assert!(P.allows_lamport_mutation(3));
2020        assert!(!P.allows_lamport_mutation(1));
2021        // An empty declared set refuses every account, a valid,
2022        // mutation-complete "no lamport writes anywhere" contract.
2023        static NONE: WritePolicy = WritePolicy::with_lamports(&[], &[]);
2024        assert!(NONE.lamports_declared());
2025        assert!(!NONE.allows_lamport_mutation(0));
2026    }
2027
2028    #[test]
2029    fn whole_account_grant_is_required_for_delegation() {
2030        static P: WritePolicy = WritePolicy::with_lamports(
2031            &[WriteRange::whole_account(0), WriteRange::new(1, 16, 8)],
2032            &[0, 1],
2033        );
2034        assert!(P.allows_whole_account_write(0));
2035        // Field-granular ranges are not a whole-account grant.
2036        assert!(!P.allows_whole_account_write(1));
2037        assert!(!P.allows_whole_account_write(2));
2038    }
2039
2040    // ── Ambient gate behavior ───────────────────────────────────────
2041
2042    use crate::account::AccountView;
2043    use hopper_native::{
2044        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
2045    };
2046
2047    fn make_account(seed: u8) -> (std::vec::Vec<u64>, AccountView<'static>) {
2048        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + 32).div_ceil(8)];
2049        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
2050        // SAFETY: the test owns `backing`, writes one valid RuntimeAccount
2051        // header, and keeps the buffer alive for the returned view.
2052        unsafe {
2053            raw.write(RuntimeAccount {
2054                borrow_state: NOT_BORROWED,
2055                is_signer: 1,
2056                is_writable: 1,
2057                executable: 0,
2058                resize_delta: 0,
2059                address: NativeAddress::new_from_array([seed; 32]),
2060                owner: NativeAddress::new_from_array([2; 32]),
2061                lamports: 100,
2062                data_len: 32,
2063            });
2064        }
2065        // SAFETY: `raw` points at the RuntimeAccount just initialized above.
2066        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
2067        (backing, AccountView::from_backend(backend))
2068    }
2069
2070    // Guarded-tier semantics: installs a data-declaring policy, which the
2071    // `unguarded-raw-surfaces` fence refuses at install (covered by its
2072    // own explicit test in that shape).
2073    #[test]
2074    #[cfg(not(feature = "unguarded-raw-surfaces"))]
2075    fn gate_refuses_undeclared_and_allows_declared_lamport_mutation() {
2076        let (_b0, a0) = make_account(10);
2077        let (_b1, a1) = make_account(11);
2078        let accounts = [a0, a1];
2079
2080        static P: WritePolicy = WritePolicy::with_lamports(&[WriteRange::whole_account(0)], &[0]);
2081
2082        // No gate installed: passthrough for everything.
2083        assert!(check_lamport_mutation(accounts[1].address()).is_ok());
2084
2085        {
2086            let _gate = install_lamport_gate(&accounts, &P);
2087            assert!(lamport_gate_active());
2088            // Declared index 0: allowed. Undeclared index 1: refused
2089            // with the indexed policy error.
2090            assert!(check_lamport_mutation(accounts[0].address()).is_ok());
2091            assert_eq!(
2092                check_lamport_mutation(accounts[1].address()),
2093                Err(write_policy_violation(1))
2094            );
2095            // An address foreign to the gated slice fails closed.
2096            let (_bf, foreign) = make_account(99);
2097            assert_eq!(
2098                check_lamport_mutation(foreign.address()),
2099                Err(write_policy_violation(u8::MAX))
2100            );
2101        }
2102
2103        // Guard dropped: gate cleared, passthrough restored.
2104        assert!(!lamport_gate_active());
2105        assert!(check_lamport_mutation(accounts[1].address()).is_ok());
2106    }
2107
2108    // Guarded-tier semantics: installs a data-declaring policy, which the
2109    // `unguarded-raw-surfaces` fence refuses at install (covered by its
2110    // own explicit test in that shape).
2111    #[test]
2112    #[cfg(not(feature = "unguarded-raw-surfaces"))]
2113    fn gate_delegation_requires_both_dimensions() {
2114        let (_b0, a0) = make_account(20);
2115        let (_b1, a1) = make_account(21);
2116        let (_b2, a2) = make_account(22);
2117        let accounts = [a0, a1, a2];
2118
2119        // 0: whole data + lamports (delegable). 1: lamports only.
2120        // 2: field range only.
2121        static P: WritePolicy = WritePolicy::with_lamports(
2122            &[WriteRange::whole_account(0), WriteRange::new(2, 16, 8)],
2123            &[0, 1],
2124        );
2125        let _gate = install_lamport_gate(&accounts, &P);
2126
2127        assert!(check_lamport_delegation(accounts[0].address()).is_ok());
2128        assert_eq!(
2129            check_lamport_delegation(accounts[1].address()),
2130            Err(write_policy_violation(1))
2131        );
2132        assert_eq!(
2133            check_lamport_delegation(accounts[2].address()),
2134            Err(write_policy_violation(2))
2135        );
2136        // Direct lamport mutation on 1 is still fine (declared).
2137        assert!(check_lamport_mutation(accounts[1].address()).is_ok());
2138    }
2139
2140    /// The runtime fence for hand-rolled installs under the size opt-out:
2141    /// a data-declaring policy is REFUSED at install (never a silently
2142    /// half-enforced gate), while a lamports-only policy installs and its
2143    /// dimensions stay fully enforced.
2144    #[test]
2145    #[cfg(feature = "unguarded-raw-surfaces")]
2146    fn unguarded_build_refuses_data_declaring_installs_loudly() {
2147        let (_b0, a0) = make_account(95);
2148        let accounts = [a0];
2149
2150        // Data-declaring policy: refused with the dedicated install error.
2151        static DATA: WritePolicy = WritePolicy::new(&[WriteRange::whole_account(0)]);
2152        assert_eq!(
2153            try_install_lamport_gate(&accounts, &DATA).map(|_| ()),
2154            Err(AMBIENT_GATE_UNGUARDED_BUILD),
2155        );
2156        assert!(!lamport_gate_active(), "a refused install leaves no gate");
2157
2158        // Lamports-only policy: installs, and the lamport dimension still
2159        // enforces (declared account 0 passes, a foreign account fails).
2160        static LAMPORTS_ONLY: WritePolicy = WritePolicy::with_lamports(&[], &[0]);
2161        let _gate = install_lamport_gate(&accounts, &LAMPORTS_ONLY);
2162        let (_bf, foreign) = make_account(96);
2163        assert!(check_lamport_mutation(accounts[0].address()).is_ok());
2164        assert_eq!(
2165            check_lamport_mutation(foreign.address()),
2166            Err(write_policy_violation(u8::MAX)),
2167        );
2168        // Writable-CPI delegation is the other dimension that SURVIVES
2169        // the opt-out: account 0 has lamport permission but no
2170        // whole-account data grant, so delegation refuses it; a foreign
2171        // account fails closed.
2172        assert_eq!(
2173            check_lamport_delegation(accounts[0].address()),
2174            Err(write_policy_violation(0)),
2175        );
2176        assert_eq!(
2177            check_lamport_delegation(foreign.address()),
2178            Err(write_policy_violation(u8::MAX)),
2179        );
2180        // The DATA and TRANSITION dimensions are compiled out with the
2181        // raw-surface guard in this build shape, the documented,
2182        // deliberate enforcement split (on the default build the same
2183        // install refuses BOTH of these on a foreign account; see
2184        // `public_raw_surfaces_are_governed_by_the_ambient_gate`). Pinned
2185        // so the split is loud, not an accident of cfg.
2186        assert!(check_data_mutation(foreign.address(), 0, 1).is_ok());
2187        assert!(check_account_transition(foreign.address()).is_ok());
2188    }
2189
2190    #[test]
2191    #[cfg(not(feature = "unguarded-raw-surfaces"))]
2192    fn public_raw_surfaces_are_governed_by_the_ambient_gate() {
2193        // The historical `strict_writes` bypass: raw `AccountView` writes
2194        // outside a `Context`. With the gate wired into the public
2195        // surfaces, a bound policy governs them too.
2196        let (_b0, a0) = make_account(90);
2197        let (_bf, foreign) = make_account(91);
2198        let accounts = [a0];
2199        // Account 0 may write ONLY bytes [8, 16) of its 32-byte data.
2200        static P: WritePolicy = WritePolicy::new(&[WriteRange::new(0, 8, 8)]);
2201
2202        // Without a gate every surface passes through untouched.
2203        {
2204            let mut reg = crate::segment_borrow::SegmentBorrowRegistry::new();
2205            assert!(accounts[0].try_borrow_mut().is_ok());
2206            assert!(accounts[0].segment_mut::<[u8; 8]>(&mut reg, 20, 8).is_ok());
2207        }
2208
2209        let _gate = install_lamport_gate(&accounts, &P);
2210
2211        // Raw whole-account borrow: the narrow declaration does not cover
2212        // the full data range, so the former bypass is refused with the
2213        // account's indexed policy error.
2214        assert_eq!(
2215            accounts[0].try_borrow_mut().map(|_| ()),
2216            Err(write_policy_violation(0)),
2217        );
2218        // A foreign account fails closed at u8::MAX.
2219        assert_eq!(
2220            foreign.try_borrow_mut().map(|_| ()),
2221            Err(write_policy_violation(u8::MAX)),
2222        );
2223
2224        // Direct segment access outside a `Context`: the declared cell is
2225        // allowed; a range shifted one byte past it is refused.
2226        let mut reg = crate::segment_borrow::SegmentBorrowRegistry::new();
2227        assert!(accounts[0].segment_mut::<[u8; 8]>(&mut reg, 8, 8).is_ok());
2228        let mut reg2 = crate::segment_borrow::SegmentBorrowRegistry::new();
2229        assert_eq!(
2230            accounts[0]
2231                .segment_mut::<[u8; 8]>(&mut reg2, 9, 8)
2232                .map(|_| ()),
2233            Err(write_policy_violation(0)),
2234        );
2235
2236        // Data-length / presence transitions on a foreign account are
2237        // refused before any native-boundary work happens.
2238        assert_eq!(foreign.resize(16), Err(write_policy_violation(u8::MAX)));
2239        assert_eq!(foreign.close(), Err(write_policy_violation(u8::MAX)));
2240        // The `close_to_unchecked` escape waives owner/writable
2241        // PREconditions but not the installed policy: the same transition
2242        // rule refuses it before any lamport moves or data zeroing.
2243        assert_eq!(
2244            foreign.close_to_unchecked(&accounts[0]),
2245            Err(write_policy_violation(u8::MAX)),
2246        );
2247    }
2248
2249    #[test]
2250    #[cfg(not(feature = "unguarded-raw-surfaces"))]
2251    fn data_only_policy_governs_data_but_passes_the_lamport_dimension() {
2252        // A data-only `strict_writes` policy (no `lamports(...)`), the shape
2253        // a BARE `strict_writes` context installs, governs the DATA
2254        // dimension (raw data mutation, transitions, out-of-set accounts)
2255        // while leaving the whole LAMPORT dimension passthrough: both direct
2256        // lamport arithmetic AND writable-CPI delegation. That carve-out is
2257        // what lets a bare-strict program keep performing writable CPIs
2258        // (System CreateAccount, Token transfer) after the gate is installed,
2259        // governing delegation would refuse every one of them, since a
2260        // data-only policy declares no lamport authority to hand a callee.
2261        let (_b0, a0) = make_account(30);
2262        let accounts = [a0];
2263        static P: WritePolicy = WritePolicy::new(&[WriteRange::whole_account(0)]);
2264        let _gate = install_lamport_gate(&accounts, &P);
2265        assert!(
2266            lamport_gate_active(),
2267            "a data-only policy installs the ambient gate"
2268        );
2269
2270        let (_bf, foreign) = make_account(31);
2271
2272        // Lamport dimension (arithmetic): undeclared, so BOTH the declared
2273        // account and a foreign one pass through.
2274        assert!(check_lamport_mutation(accounts[0].address()).is_ok());
2275        assert!(check_lamport_mutation(foreign.address()).is_ok());
2276
2277        // Lamport dimension (writable-CPI delegation): also passthrough for a
2278        // data-only policy, the backward-compat carve-out, so bare-strict
2279        // programs can still delegate accounts to CPI callees.
2280        assert!(check_lamport_delegation(accounts[0].address()).is_ok());
2281        assert!(check_lamport_delegation(foreign.address()).is_ok());
2282
2283        // Data dimension IS governed: the declared whole-account write is
2284        // permitted, but a foreign account's data write is refused fail-closed.
2285        assert!(check_data_mutation(accounts[0].address(), 0, 1).is_ok());
2286        assert_eq!(
2287            check_data_mutation(foreign.address(), 0, 1),
2288            Err(write_policy_violation(u8::MAX)),
2289        );
2290        // Transitions on a foreign account are refused; on the declared
2291        // (whole-account) one, permitted.
2292        assert!(check_account_transition(accounts[0].address()).is_ok());
2293        assert_eq!(
2294            check_account_transition(foreign.address()),
2295            Err(write_policy_violation(u8::MAX)),
2296        );
2297    }
2298
2299    #[test]
2300    #[cfg(not(feature = "unguarded-raw-surfaces"))]
2301    fn mutation_complete_policy_still_governs_delegation() {
2302        // The delegation carve-out is scoped to data-only policies by
2303        // `!lamports_declared()`. A `mutation_complete` policy DECLARES the
2304        // lamport dimension, so delegation stays governed: the declared
2305        // whole-account+lamport account is permitted, a lamport-but-not-data
2306        // account is refused, and a foreign one fails closed.
2307        let (_b0, a0) = make_account(32); // whole-account data + lamport
2308        let (_b1, a1) = make_account(33); // lamport only, no data grant
2309        let accounts = [a0, a1];
2310        static P: WritePolicy =
2311            WritePolicy::with_lamports(&[WriteRange::whole_account(0)], &[0, 1]);
2312        let _gate = install_lamport_gate(&accounts, &P);
2313
2314        let (_bf, foreign) = make_account(34);
2315        // Account 0: lamport-declared AND whole-account data grant -> may be
2316        // delegated.
2317        assert!(check_lamport_delegation(accounts[0].address()).is_ok());
2318        // Account 1: lamport-declared but no whole-account data grant ->
2319        // delegation refused with its own index.
2320        assert_eq!(
2321            check_lamport_delegation(accounts[1].address()),
2322            Err(write_policy_violation(1)),
2323        );
2324        // Foreign: fails closed.
2325        assert_eq!(
2326            check_lamport_delegation(foreign.address()),
2327            Err(write_policy_violation(u8::MAX)),
2328        );
2329    }
2330
2331    #[test]
2332    fn nested_gates_shadow_and_resume_like_a_stack() {
2333        let (_b0, a0) = make_account(40);
2334        let (_b1, a1) = make_account(41);
2335        let outer_accounts = [a0];
2336        let inner_accounts = [a1];
2337        static OUTER: WritePolicy = WritePolicy::with_lamports(&[], &[0]);
2338        static INNER: WritePolicy = WritePolicy::with_lamports(&[], &[]);
2339
2340        let _outer = install_lamport_gate(&outer_accounts, &OUTER);
2341        assert!(check_lamport_mutation(outer_accounts[0].address()).is_ok());
2342        {
2343            let _inner = install_lamport_gate(&inner_accounts, &INNER);
2344            // The inner gate (highest token) shadows the outer one:
2345            // inner's empty set refuses its own account 0...
2346            assert_eq!(
2347                check_lamport_mutation(inner_accounts[0].address()),
2348                Err(write_policy_violation(0))
2349            );
2350        }
2351        // ...and dropping the inner guard frees only its slot, so the
2352        // outer gate resumes governing.
2353        assert!(lamport_gate_active());
2354        assert!(check_lamport_mutation(outer_accounts[0].address()).is_ok());
2355    }
2356
2357    // ── Redesign regression tests (value store, tokens, fail-closed) ─
2358
2359    // Guarded-tier semantics: installs a data-declaring policy, which the
2360    // `unguarded-raw-surfaces` fence refuses at install (covered by its
2361    // own explicit test in that shape).
2362    #[test]
2363    #[cfg(not(feature = "unguarded-raw-surfaces"))]
2364    fn forgotten_guard_leaves_stale_value_policy_never_ub() {
2365        static P: WritePolicy = WritePolicy::with_lamports(&[WriteRange::whole_account(0)], &[0]);
2366        let stale_address;
2367        {
2368            let (_b0, a0) = make_account(50);
2369            let accounts = [a0];
2370            stale_address = *accounts[0].address();
2371            let guard = install_lamport_gate(&accounts, &P);
2372            // Safe code can always skip Drop. With the value store this
2373            // leaks a slot; the old pointer store turned every later
2374            // check into a dangling dereference.
2375            core::mem::forget(guard);
2376            // `accounts` and its backing buffer are freed here.
2377        }
2378
2379        // The stale gate is still installed, as VALUES, so probing it
2380        // after the accounts are gone is plain comparison, not UB.
2381        assert!(lamport_gate_active());
2382
2383        // A fresh, unrelated account is foreign to the stale policy:
2384        // fail closed (observable stale enforcement, the documented
2385        // residual of leaking a guard).
2386        let (_bf, fresh) = make_account(51);
2387        assert_eq!(
2388            check_lamport_mutation(fresh.address()),
2389            Err(write_policy_violation(u8::MAX))
2390        );
2391
2392        // An address equal to the stale entry is governed by the stale
2393        // permission bits (allowed here), stale-value semantics, by
2394        // value, no liveness required.
2395        assert!(check_lamport_mutation(&stale_address).is_ok());
2396
2397        // The leaked slot stays occupied for this thread-local tier;
2398        // the test thread ends here, taking the store with it.
2399    }
2400
2401    #[test]
2402    fn out_of_order_guard_drops_cannot_corrupt_other_gates() {
2403        let (_b0, a0) = make_account(60);
2404        let (_b1, a1) = make_account(61);
2405        let outer_accounts = [a0];
2406        let inner_accounts = [a1];
2407        static OUTER: WritePolicy = WritePolicy::with_lamports(&[], &[0]);
2408        static INNER: WritePolicy = WritePolicy::with_lamports(&[], &[]);
2409
2410        let outer = install_lamport_gate(&outer_accounts, &OUTER);
2411        let inner = install_lamport_gate(&inner_accounts, &INNER);
2412
2413        // Inner (most recently installed) governs while both are alive.
2414        assert_eq!(
2415            check_lamport_mutation(inner_accounts[0].address()),
2416            Err(write_policy_violation(0))
2417        );
2418
2419        // Drop the OUTER guard first, out of creation order. Under the
2420        // old prev-chain this restored a stale snapshot over the live
2421        // inner gate; under the token store it frees only outer's slot.
2422        drop(outer);
2423        assert!(lamport_gate_active());
2424        assert_eq!(
2425            check_lamport_mutation(inner_accounts[0].address()),
2426            Err(write_policy_violation(0))
2427        );
2428        // Outer's account is now foreign to the (still governing) inner
2429        // gate, fail closed, not fail open.
2430        assert_eq!(
2431            check_lamport_mutation(outer_accounts[0].address()),
2432            Err(write_policy_violation(u8::MAX))
2433        );
2434
2435        // Dropping inner empties the store: passthrough, nothing stale.
2436        drop(inner);
2437        assert!(!lamport_gate_active());
2438        assert!(check_lamport_mutation(outer_accounts[0].address()).is_ok());
2439    }
2440
2441    #[test]
2442    fn depth_exhaustion_fails_closed_loudly() {
2443        static P: WritePolicy = WritePolicy::with_lamports(&[], &[]);
2444        let (_b, a) = make_account(70);
2445        let accounts = [a];
2446        let _g1 = try_install_lamport_gate(&accounts, &P).unwrap();
2447        let _g2 = try_install_lamport_gate(&accounts, &P).unwrap();
2448        let _g3 = try_install_lamport_gate(&accounts, &P).unwrap();
2449        let _g4 = try_install_lamport_gate(&accounts, &P).unwrap();
2450        // All LAMPORT_GATE_DEPTH slots occupied: the next install is
2451        // refused loudly, nothing is evicted or corrupted.
2452        assert_eq!(
2453            try_install_lamport_gate(&accounts, &P).unwrap_err(),
2454            LAMPORT_GATE_DEPTH_EXCEEDED
2455        );
2456        // The existing gates keep enforcing.
2457        assert_eq!(
2458            check_lamport_mutation(accounts[0].address()),
2459            Err(write_policy_violation(0))
2460        );
2461    }
2462
2463    #[test]
2464    fn install_fails_closed_when_accounts_exceed_capacity() {
2465        static P: WritePolicy = WritePolicy::with_lamports(&[], &[]);
2466        let mut backings = std::vec::Vec::new();
2467        let mut views = std::vec::Vec::new();
2468        let mut i = 0;
2469        while i < LAMPORT_GATE_CAPACITY + 1 {
2470            let (backing, view) = make_account((i % 256) as u8);
2471            backings.push(backing);
2472            views.push(view);
2473            i += 1;
2474        }
2475        // More accounts than the gate can copy: refuse loudly rather
2476        // than silently truncate the governed set.
2477        assert_eq!(
2478            try_install_lamport_gate(&views, &P).unwrap_err(),
2479            LAMPORT_GATE_TOO_MANY_ACCOUNTS
2480        );
2481        assert!(!lamport_gate_active());
2482    }
2483
2484    #[test]
2485    fn duplicate_addresses_share_one_merged_permission() {
2486        // Two views with the SAME address at indices 0 and 1; lamports
2487        // declared only on index 1. Permission is a property of the
2488        // account (the loader hands duplicate metas one RuntimeAccount),
2489        // so the merged entry allows mutation through either position.
2490        let (_b0, a0) = make_account(75);
2491        let (_b1, a1) = make_account(75);
2492        let accounts = [a0, a1];
2493        static P: WritePolicy = WritePolicy::with_lamports(&[], &[1]);
2494        let _gate = install_lamport_gate(&accounts, &P);
2495        assert!(check_lamport_mutation(accounts[0].address()).is_ok());
2496        assert!(check_lamport_mutation(accounts[1].address()).is_ok());
2497    }
2498
2499    #[test]
2500    fn fallback_tier_second_install_is_refused_fail_closed() {
2501        // Direct exercise of the host fallback tier's single-slot store
2502        // (the process-global tier is not active under `test`, but its
2503        // store type is compiled and its semantics are tier-independent:
2504        // GateStore<1> + spinlock).
2505        static P: WritePolicy = WritePolicy::with_lamports(&[], &[0]);
2506        let (_b0, a0) = make_account(80);
2507        let (_b1, a1) = make_account(81);
2508        let first_accounts = [a0];
2509        let second_accounts = [a1];
2510
2511        let store = SpinlockGateStore::new();
2512        let token = store
2513            .with_lock(|s| s.install_with_args(&first_accounts, &P, &[]))
2514            .unwrap();
2515
2516        // Second install while one gate is active: refused loudly (the
2517        // tier maps NoFreeSlot to LAMPORT_GATE_CONTENDED), never shared
2518        // and never corrupting the first gate.
2519        assert_eq!(
2520            store
2521                .with_lock(|s| s.install_with_args(&second_accounts, &P, &[]))
2522                .unwrap_err(),
2523            GateInstallError::NoFreeSlot
2524        );
2525
2526        // The first gate still enforces, entirely under the lock.
2527        store.with_lock(|s| {
2528            assert!(s
2529                .check(first_accounts[0].address(), GateCheck::Lamports)
2530                .is_ok());
2531            assert_eq!(
2532                s.check(second_accounts[0].address(), GateCheck::Lamports),
2533                Err(write_policy_violation(u8::MAX))
2534            );
2535        });
2536
2537        // Releasing the first gate frees the slot for the next install.
2538        store.with_lock(|s| s.remove(token));
2539        assert!(store
2540            .with_lock(|s| s.install_with_args(&second_accounts, &P, &[]))
2541            .is_ok());
2542    }
2543}