Skip to main content

hopper_core/check/
trust.rs

1//! Foreign-account trust profiles.
2//!
3//! Configurable validation policies for loading accounts owned by external
4//! programs. Each profile defines which checks to enforce, allowing
5//! programs to explicitly declare their trust assumptions.
6//!
7//! ## Trust Levels
8//!
9//! - **Strict**: owner + layout_id + exact size + not frozen/closed
10//! - **Compatible**: owner + layout_id + minimum size (supports newer versions)
11//! - **Observational**: layout_id only, best-effort (indexers/tooling)
12//!
13//! ```ignore
14//! let profile = TrustProfile::strict(&KNOWN_PROGRAM_ID, &MyLayout::LAYOUT_ID, MyLayout::LEN);
15//! let data = profile.load(account)?;
16//! let overlay = MyLayout::overlay(data)?;
17//! ```
18
19use hopper_runtime::error::ProgramError;
20use hopper_runtime::{AccountView, Address, Ref};
21
22/// Trust level for foreign account validation.
23#[derive(Clone, Copy, PartialEq, Eq)]
24pub enum TrustLevel {
25    /// Full validation: owner + layout_id + exact size + not closed.
26    Strict,
27    /// Version-compatible: owner + layout_id + minimum size.
28    Compatible,
29    /// Best-effort: layout_id match only. For tooling and indexers.
30    Observational,
31}
32
33/// Policy flags for additional constraints.
34#[derive(Clone, Copy)]
35pub struct TrustFlags {
36    /// Reject accounts with the close sentinel (disc == 0xFF).
37    pub reject_closed: bool,
38    /// Require the account to be immutable (not writable).
39    pub require_immutable: bool,
40    /// Minimum version (byte 1 of header). 0 = no minimum.
41    pub min_version: u8,
42}
43
44impl TrustFlags {
45    /// Default flags: reject closed, no immutability requirement, no version floor.
46    #[inline(always)]
47    pub const fn default() -> Self {
48        Self {
49            reject_closed: true,
50            require_immutable: false,
51            min_version: 0,
52        }
53    }
54
55    /// Paranoid mode: reject closed + require immutable.
56    #[inline(always)]
57    pub const fn paranoid() -> Self {
58        Self {
59            reject_closed: true,
60            require_immutable: true,
61            min_version: 0,
62        }
63    }
64}
65
66/// A foreign-account trust profile.
67///
68/// Encapsulates the expected owner, layout_id, size, and trust level
69/// so that foreign account loading is explicit and auditable.
70pub struct TrustProfile<'a> {
71    /// Expected owner program.
72    pub owner: &'a Address,
73    /// Expected layout_id (first 8 bytes of SHA-256 hash).
74    pub layout_id: &'a [u8; 8],
75    /// Expected size (exact for Strict, minimum for Compatible, ignored for Observational).
76    pub size: usize,
77    /// Trust level.
78    pub level: TrustLevel,
79    /// Additional flags.
80    pub flags: TrustFlags,
81}
82
83impl<'a> TrustProfile<'a> {
84    /// Strict profile: full validation.
85    #[inline(always)]
86    pub const fn strict(owner: &'a Address, layout_id: &'a [u8; 8], size: usize) -> Self {
87        Self {
88            owner,
89            layout_id,
90            size,
91            level: TrustLevel::Strict,
92            flags: TrustFlags::default(),
93        }
94    }
95
96    /// Compatible profile: accepts newer versions with larger accounts.
97    #[inline(always)]
98    pub const fn compatible(owner: &'a Address, layout_id: &'a [u8; 8], min_size: usize) -> Self {
99        Self {
100            owner,
101            layout_id,
102            size: min_size,
103            level: TrustLevel::Compatible,
104            flags: TrustFlags::default(),
105        }
106    }
107
108    /// Observational profile: layout_id only, for tooling.
109    #[inline(always)]
110    pub const fn observational(layout_id: &'a [u8; 8]) -> Self {
111        // Observational mode: zeroed address is intentional -- owner check
112        // is skipped by load_observational(), so this value is never read.
113        const ZERO_ADDR: Address = Address::new_from_array([0u8; 32]);
114        Self {
115            owner: &ZERO_ADDR,
116            layout_id,
117            size: 0,
118            level: TrustLevel::Observational,
119            flags: TrustFlags {
120                reject_closed: false,
121                require_immutable: false,
122                min_version: 0,
123            },
124        }
125    }
126
127    /// Read-only profile: owner + layout_id + minimum size + require immutable.
128    ///
129    /// Like `compatible()` but additionally requires the account to not be
130    /// writable. Use this when reading cross-program state that must not
131    /// be mutated within the same transaction.
132    #[inline(always)]
133    pub const fn read_only(owner: &'a Address, layout_id: &'a [u8; 8], min_size: usize) -> Self {
134        Self {
135            owner,
136            layout_id,
137            size: min_size,
138            level: TrustLevel::Compatible,
139            flags: TrustFlags {
140                reject_closed: true,
141                require_immutable: true,
142                min_version: 0,
143            },
144        }
145    }
146
147    /// Set the minimum version floor.
148    #[inline(always)]
149    pub const fn with_min_version(mut self, v: u8) -> Self {
150        self.flags.min_version = v;
151        self
152    }
153
154    /// Require the account to be immutable (not writable).
155    #[inline(always)]
156    pub const fn require_immutable(mut self) -> Self {
157        self.flags.require_immutable = true;
158        self
159    }
160
161    /// Validate an account against this profile and return its data.
162    ///
163    /// On success, returns a borrow-carrying byte view suitable for overlay.
164    #[inline]
165    pub fn load(&self, account: &'a AccountView) -> Result<Ref<'a, [u8]>, ProgramError> {
166        // Immutability check (if required).
167        if self.flags.require_immutable && account.is_writable() {
168            return Err(ProgramError::InvalidAccountData);
169        }
170
171        match self.level {
172            TrustLevel::Strict => self.load_strict(account),
173            TrustLevel::Compatible => self.load_compatible(account),
174            TrustLevel::Observational => self.load_observational(account),
175        }
176    }
177
178    #[inline]
179    fn load_strict(&self, account: &'a AccountView) -> Result<Ref<'a, [u8]>, ProgramError> {
180        // Owner check.
181        if !account.owned_by(self.owner) {
182            return Err(ProgramError::IncorrectProgramId);
183        }
184        let data = account.try_borrow()?;
185        // Exact size check.
186        if data.len() != self.size {
187            return Err(ProgramError::AccountDataTooSmall);
188        }
189        // Layout ID check.
190        self.check_layout_id(&data)?;
191        // Close sentinel check.
192        if self.flags.reject_closed {
193            self.check_not_closed(&data)?;
194        }
195        // Version floor check.
196        if self.flags.min_version > 0 {
197            self.check_min_version(&data)?;
198        }
199        Ok(data)
200    }
201
202    #[inline]
203    fn load_compatible(&self, account: &'a AccountView) -> Result<Ref<'a, [u8]>, ProgramError> {
204        if !account.owned_by(self.owner) {
205            return Err(ProgramError::IncorrectProgramId);
206        }
207        let data = account.try_borrow()?;
208        // Minimum size check (account may be larger than expected).
209        if data.len() < self.size {
210            return Err(ProgramError::AccountDataTooSmall);
211        }
212        self.check_layout_id(&data)?;
213        if self.flags.reject_closed {
214            self.check_not_closed(&data)?;
215        }
216        if self.flags.min_version > 0 {
217            self.check_min_version(&data)?;
218        }
219        Ok(data)
220    }
221
222    #[inline]
223    fn load_observational(&self, account: &'a AccountView) -> Result<Ref<'a, [u8]>, ProgramError> {
224        let data = account.try_borrow()?;
225        if data.len() < crate::account::HEADER_LEN {
226            return Err(ProgramError::AccountDataTooSmall);
227        }
228        self.check_layout_id(&data)?;
229        Ok(data)
230    }
231
232    /// Check the layout_id in the header matches expected.
233    #[inline(always)]
234    fn check_layout_id(&self, data: &[u8]) -> Result<(), ProgramError> {
235        if data.len() < 12 {
236            return Err(ProgramError::AccountDataTooSmall);
237        }
238        if data[4..12] != *self.layout_id {
239            return Err(ProgramError::InvalidAccountData);
240        }
241        Ok(())
242    }
243
244    /// Check the account is not closed (disc != CLOSE_SENTINEL).
245    #[inline(always)]
246    fn check_not_closed(&self, data: &[u8]) -> Result<(), ProgramError> {
247        if !data.is_empty() && data[0] == crate::account::CLOSE_SENTINEL {
248            return Err(ProgramError::InvalidAccountData);
249        }
250        Ok(())
251    }
252
253    /// Check version meets the floor.
254    #[inline(always)]
255    fn check_min_version(&self, data: &[u8]) -> Result<(), ProgramError> {
256        if data.len() < 2 {
257            return Err(ProgramError::AccountDataTooSmall);
258        }
259        if data[1] < self.flags.min_version {
260            return Err(ProgramError::InvalidAccountData);
261        }
262        Ok(())
263    }
264}
265
266/// Load a foreign account with a trust profile, returning a typed overlay.
267///
268/// Convenience function combining profile validation with Pod overlay.
269#[inline]
270pub fn load_foreign_with_profile<'a, T: crate::account::Pod + crate::account::FixedLayout>(
271    account: &'a AccountView,
272    profile: &TrustProfile<'a>,
273) -> Result<crate::account::VerifiedAccount<'a, T>, ProgramError> {
274    let data = profile.load(account)?;
275    crate::account::VerifiedAccount::from_ref(data)
276}