Skip to main content

hopper_runtime/
foreign.rs

1//! Manifest-backed foreign-account lenses.
2//!
3//! This module provides manifest-backed foreign-account lenses as a
4//! verifiable alternative to ad-hoc offset-based foreign reads.
5//!
6//! # Problem
7//!
8//! Today, reading a field from an account owned by a *different* program
9//! either imports the foreign program's crate (tight coupling, forces
10//! version-lock) or reads raw bytes by hand-maintained offset
11//! (no ABI-drift detection. if the foreign program changes its layout,
12//! silent misreads result).
13//!
14//! # Design
15//!
16//! A `ForeignManifest` is an opaque witness (supplied by the caller)
17//! that carries the foreign program's `wire_fp64` hash plus the layout
18//! discriminator it expects for a particular `T: AccountLayout`. When
19//! `ctx.foreign::<T>(idx, &manifest)?` is called:
20//!
21//! 1. The account's owner must match `manifest.program_id`
22//! 2. The account's header discriminator must match `T::DISC` and
23//!    `manifest.expected_disc`
24//! 3. The header's `wire_fp64` must match `T::WIRE_FINGERPRINT` and
25//!    `manifest.expected_wire_fp`
26//! 4. `schema_epoch` must fall in `manifest.supported_epochs`
27//!
28//! Only after all four pass does the lens expose field access. Any
29//! mismatch returns `ProgramError::InvalidAccountData`. never silent
30//! mis-reads, never UB.
31//!
32//! # Manifest sourcing
33//!
34//! Hopper does not fetch manifests from RPC inside a program (that
35//! would be round-trip CPI with no caching story). Manifests are
36//! caller-supplied, typically from:
37//!
38//! - An embedded `const ForeignManifest` authored when the program was
39//!   built (works when the foreign program's ABI is known at build time)
40//! - A manifest account located at the canonical manifest PDA
41//!   (`find_program_address(&[MANIFEST_SEED], &foreign_program_id)`)
42//!   whose payload has already been verified by a prior instruction
43//! - A Hopper IDL that emits manifest constants as part of
44//!   its client-generation output
45
46use crate::account::AccountView;
47use crate::address::Address;
48use crate::borrow::Ref;
49use crate::crypto::{sha256_single, Sha256Hash};
50use crate::error::ProgramError;
51use crate::layout::{HopperHeader, LayoutContract};
52use crate::zerocopy::{AccountLayout, ZeroCopy};
53use crate::ProgramResult;
54use core::marker::PhantomData;
55
56/// Validation contract for known non-Hopper account layouts.
57///
58/// Hopper-owned layouts use Hopper headers and [`Account`](crate::Account).
59/// Known foreign accounts usually do not. Implement this trait on a marker or
60/// adapter type to validate owner, fixed byte prefix/discriminator, minimum
61/// length, version bytes, oracle freshness gates, or any other external
62/// invariant before binding an [`ExternalAccount`].
63pub trait ExternalZeroCopy {
64    /// Guard-owned zero-copy view returned by this external adapter.
65    ///
66    /// Implementations should store the supplied [`Ref<'a, [u8]>`] directly or
67    /// project it into a narrower borrowed view. This keeps the account-data
68    /// borrow alive for exactly as long as the external view exists.
69    type View<'a>;
70
71    /// Single expected owner program, when the adapter has one.
72    const OWNER: Option<Address> = None;
73    /// Optional byte prefix/discriminator at offset 0.
74    const DISCRIMINATOR: Option<&'static [u8]> = None;
75    /// Minimum account data length accepted by this adapter.
76    const MIN_LEN: usize = 0;
77
78    /// Validate this external account. Override for multi-owner layouts,
79    /// versioned dispatch, or custom invariants.
80    #[inline]
81    fn validate(view: &AccountView<'_>) -> ProgramResult {
82        if let Some(owner) = Self::OWNER {
83            view.check_owned_by(&owner)?;
84        }
85        if view.data_len() < Self::MIN_LEN {
86            return Err(ProgramError::AccountDataTooSmall);
87        }
88        if let Some(discriminator) = Self::DISCRIMINATOR {
89            let data = view.try_borrow()?;
90            if data.len() < discriminator.len() {
91                return Err(ProgramError::AccountDataTooSmall);
92            }
93            if !data.starts_with(discriminator) {
94                return Err(ProgramError::InvalidAccountData);
95            }
96        }
97        Ok(())
98    }
99
100    /// Build the adapter's typed zero-copy view from an active account-data
101    /// borrow. The borrow guard is consumed so the returned view can carry it.
102    fn view<'a>(data: Ref<'a, [u8]>) -> Result<Self::View<'a>, ProgramError>;
103}
104
105/// Minimal guard-owned external byte view.
106///
107/// Adapters that only need checked bytes can use this as their `View<'a>` while
108/// richer adapters can expose accessor methods over the same borrowed data.
109pub struct ExternalBytes<'a> {
110    data: Ref<'a, [u8]>,
111}
112
113impl<'a> ExternalBytes<'a> {
114    /// Wrap an active external account-data borrow.
115    #[inline(always)]
116    pub const fn new(data: Ref<'a, [u8]>) -> Self {
117        Self { data }
118    }
119
120    /// Borrow the validated external bytes.
121    #[inline(always)]
122    pub fn as_bytes(&self) -> &[u8] {
123        &self.data
124    }
125}
126
127impl core::ops::Deref for ExternalBytes<'_> {
128    type Target = [u8];
129
130    #[inline(always)]
131    fn deref(&self) -> &[u8] {
132        self.as_bytes()
133    }
134}
135
136/// Owner/discriminator-selected resolver for external account families.
137///
138/// Use this for account sets such as Pyth/Switchboard/custom oracle unions or
139/// Token/Token-2022 interfaces where the account owner decides which zero-copy
140/// view should be used at runtime.
141pub trait ExternalResolve {
142    /// Guard-owned resolved view.
143    type Resolved<'a>;
144
145    /// Resolve the account into one of the supported external views.
146    fn resolve<'a>(view: &'a AccountView<'a>) -> Result<Self::Resolved<'a>, ProgramError>;
147}
148
149/// Adapter-specific proof verifier for known external accounts.
150///
151/// Proof implementations should perform one focused validation step, such as
152/// "this token account has the expected mint" or "this oracle price is fresh".
153/// The returned proof token can be carried into downstream APIs that should not
154/// accept a merely raw or adapter-checked account. The token proves the bytes
155/// observed when [`ExternalProof::verify`] ran; it must not be carried across a
156/// CPI that can mutate the account. Re-run [`ExternalAccount::checked`] after
157/// such a CPI. Basic adapter validation is independently repeated by every
158/// subsequent safe data/view/resolve/explain access.
159pub trait ExternalProof<T: ExternalZeroCopy> {
160    /// Proof token produced by this verifier.
161    type Proof<'a>;
162
163    /// Verify `account` and return the proof token.
164    fn verify<'a>(account: ExternalAccount<'a, T>) -> Result<Self::Proof<'a>, ProgramError>;
165}
166
167/// External account paired with an adapter-specific point-in-time proof token.
168pub struct ExternalChecked<'info, T, P>
169where
170    T: ExternalZeroCopy,
171    P: ExternalProof<T>,
172{
173    account: ExternalAccount<'info, T>,
174    proof: P::Proof<'info>,
175    _marker: PhantomData<P>,
176}
177
178impl<'info, T, P> ExternalChecked<'info, T, P>
179where
180    T: ExternalZeroCopy,
181    P: ExternalProof<T>,
182{
183    /// The checked external account.
184    #[inline(always)]
185    pub const fn account(&self) -> ExternalAccount<'info, T> {
186        self.account
187    }
188
189    /// The adapter-specific proof token.
190    #[inline(always)]
191    pub const fn proof(&self) -> &P::Proof<'info> {
192        &self.proof
193    }
194
195    /// Borrow and decode the adapter's typed zero-copy view.
196    #[inline]
197    pub fn view(&self) -> Result<T::View<'info>, ProgramError> {
198        self.account.view()
199    }
200}
201
202/// Minimal no-allocation sink for external explain adapters.
203///
204/// Runtime adapters can emit structured fields without depending on a concrete
205/// CLI/SVM explain representation. Sinks may redact, hash, serialize, or ignore
206/// fields according to their environment.
207pub trait ExternalExplainSink {
208    fn field_str(&mut self, name: &'static str, value: &str) -> ProgramResult {
209        let _ = (name, value);
210        Ok(())
211    }
212
213    fn field_bytes(&mut self, name: &'static str, value: &[u8]) -> ProgramResult {
214        let _ = (name, value);
215        Ok(())
216    }
217
218    fn field_address(&mut self, name: &'static str, value: &Address) -> ProgramResult {
219        let _ = (name, value);
220        Ok(())
221    }
222
223    fn field_u64(&mut self, name: &'static str, value: u64) -> ProgramResult {
224        let _ = (name, value);
225        Ok(())
226    }
227
228    fn field_i64(&mut self, name: &'static str, value: i64) -> ProgramResult {
229        let _ = (name, value);
230        Ok(())
231    }
232
233    fn field_bool(&mut self, name: &'static str, value: bool) -> ProgramResult {
234        let _ = (name, value);
235        Ok(())
236    }
237}
238
239/// Optional structured explain hook for external account adapters.
240pub trait ExplainExternal: ExternalZeroCopy {
241    /// Emit adapter-specific explain fields.
242    fn explain<S: ExternalExplainSink>(account: &AccountView<'_>, sink: &mut S) -> ProgramResult;
243}
244
245/// Copyable value that can be read from a checked external-account byte lens.
246pub trait ExternalLensValue: Sized {
247    /// Number of bytes consumed by this lens value.
248    const SIZE: usize;
249
250    /// Read a value from exactly [`Self::SIZE`] bytes.
251    fn read(bytes: &[u8]) -> Self;
252}
253
254macro_rules! impl_external_lens_value_le {
255    ($ty:ty, $size:expr) => {
256        impl ExternalLensValue for $ty {
257            const SIZE: usize = $size;
258
259            #[inline(always)]
260            fn read(bytes: &[u8]) -> Self {
261                let mut raw = [0u8; $size];
262                raw.copy_from_slice(bytes);
263                <$ty>::from_le_bytes(raw)
264            }
265        }
266    };
267}
268
269impl ExternalLensValue for u8 {
270    const SIZE: usize = 1;
271
272    #[inline(always)]
273    fn read(bytes: &[u8]) -> Self {
274        bytes[0]
275    }
276}
277
278impl ExternalLensValue for i8 {
279    const SIZE: usize = 1;
280
281    #[inline(always)]
282    fn read(bytes: &[u8]) -> Self {
283        bytes[0] as i8
284    }
285}
286
287impl_external_lens_value_le!(u16, 2);
288impl_external_lens_value_le!(u32, 4);
289impl_external_lens_value_le!(u64, 8);
290impl_external_lens_value_le!(u128, 16);
291impl_external_lens_value_le!(i16, 2);
292impl_external_lens_value_le!(i32, 4);
293impl_external_lens_value_le!(i64, 8);
294impl_external_lens_value_le!(i128, 16);
295
296impl<const N: usize> ExternalLensValue for [u8; N] {
297    const SIZE: usize = N;
298
299    #[inline(always)]
300    fn read(bytes: &[u8]) -> Self {
301        let mut raw = [0u8; N];
302        raw.copy_from_slice(bytes);
303        raw
304    }
305}
306
307impl ExternalLensValue for Address {
308    const SIZE: usize = 32;
309
310    #[inline(always)]
311    fn read(bytes: &[u8]) -> Self {
312        let mut raw = [0u8; 32];
313        raw.copy_from_slice(bytes);
314        Address::new_from_array(raw)
315    }
316}
317
318/// Bounds-checked zero-copy byte lens into an external account.
319pub struct ExternalLens<'a, V: ExternalLensValue, const OFFSET: usize> {
320    data: Ref<'a, [u8]>,
321    _value: PhantomData<V>,
322}
323
324impl<'a, V: ExternalLensValue, const OFFSET: usize> ExternalLens<'a, V, OFFSET> {
325    #[inline]
326    fn new(data: Ref<'a, [u8]>) -> Result<Self, ProgramError> {
327        let end = OFFSET
328            .checked_add(V::SIZE)
329            .ok_or(ProgramError::ArithmeticOverflow)?;
330        if end > data.len() {
331            return Err(ProgramError::AccountDataTooSmall);
332        }
333        Ok(Self {
334            data,
335            _value: PhantomData,
336        })
337    }
338
339    /// Borrow the checked byte range backing this lens.
340    #[inline(always)]
341    pub fn as_bytes(&self) -> &[u8] {
342        &self.data[OFFSET..OFFSET + V::SIZE]
343    }
344
345    /// Read the lens value by copy.
346    #[inline(always)]
347    pub fn get(&self) -> V {
348        V::read(self.as_bytes())
349    }
350}
351
352/// Validated handle to a known external account.
353///
354/// This wrapper is intentionally transparent over [`AccountView`]. It proves
355/// the adapter's [`ExternalZeroCopy::validate`] contract, but it does not imply
356/// a Hopper header is present. Use [`ExternalAccount::data`] or adapter helper
357/// methods to read bytes, and keep raw `AccountView`/`UncheckedAccount` for
358/// accounts that truly have no known schema.
359#[repr(transparent)]
360pub struct ExternalAccount<'info, T: ExternalZeroCopy> {
361    inner: &'info AccountView<'info>,
362    _ty: PhantomData<T>,
363}
364
365impl<'info, T: ExternalZeroCopy> Clone for ExternalAccount<'info, T> {
366    fn clone(&self) -> Self {
367        *self
368    }
369}
370impl<'info, T: ExternalZeroCopy> Copy for ExternalAccount<'info, T> {}
371
372impl<T: ExternalZeroCopy> core::fmt::Debug for ExternalAccount<'_, T> {
373    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
374        f.debug_struct("ExternalAccount")
375            .field("key", self.key())
376            .field("owner", &self.owner())
377            .field("data_len", &self.data_len())
378            .finish()
379    }
380}
381
382impl<'info, T: ExternalZeroCopy> ExternalAccount<'info, T> {
383    #[inline(always)]
384    fn revalidate(&self) -> ProgramResult {
385        T::validate(self.inner)
386    }
387
388    /// Wrap an account that has already been validated by `T`.
389    #[inline(always)]
390    ///
391    /// # Safety
392    ///
393    /// Caller must have verified `T::validate(view)` for this account.
394    pub unsafe fn new_unchecked(view: &'info AccountView<'info>) -> Self {
395        Self {
396            inner: view,
397            _ty: PhantomData,
398        }
399    }
400
401    /// Validate and bind a known external account.
402    #[inline]
403    pub fn try_new(view: &'info AccountView<'info>) -> Result<Self, ProgramError> {
404        T::validate(view)?;
405        Ok(Self {
406            inner: view,
407            _ty: PhantomData,
408        })
409    }
410
411    /// The underlying account view.
412    #[inline(always)]
413    pub fn as_account(&self) -> &'info AccountView<'info> {
414        self.inner
415    }
416
417    /// The account public key.
418    #[inline(always)]
419    pub fn key(&self) -> &Address {
420        self.inner.address()
421    }
422
423    /// The owning program, copied out of the account header.
424    #[inline(always)]
425    pub fn owner(&self) -> Address {
426        self.inner.read_owner()
427    }
428
429    /// Current external account data length.
430    #[inline(always)]
431    pub fn data_len(&self) -> usize {
432        self.inner.data_len()
433    }
434
435    /// Borrow the external account bytes after adapter validation.
436    #[inline(always)]
437    pub fn data(&self) -> Result<Ref<'info, [u8]>, ProgramError> {
438        self.revalidate()?;
439        self.inner.try_borrow()
440    }
441
442    /// Borrow the bytes for the duration of a closure.
443    #[inline]
444    pub fn with_data<R, F>(&self, f: F) -> Result<R, ProgramError>
445    where
446        F: FnOnce(&[u8]) -> Result<R, ProgramError>,
447    {
448        let data = self.data()?;
449        f(&data)
450    }
451
452    /// Borrow and decode the adapter's typed zero-copy view.
453    #[inline]
454    pub fn view(&self) -> Result<T::View<'info>, ProgramError> {
455        T::view(self.data()?)
456    }
457
458    /// Borrow the typed zero-copy view for the duration of a closure.
459    #[inline]
460    pub fn with_view<R, F>(&self, f: F) -> Result<R, ProgramError>
461    where
462        F: FnOnce(T::View<'info>) -> Result<R, ProgramError>,
463    {
464        f(self.view()?)
465    }
466
467    /// Verify an adapter-specific proof and carry its token with the account.
468    #[inline]
469    pub fn checked<P>(self) -> Result<ExternalChecked<'info, T, P>, ProgramError>
470    where
471        P: ExternalProof<T>,
472    {
473        // A proof adapter may intentionally verify only one focused business
474        // invariant and need not borrow the typed view itself. Re-establish the
475        // base owner/discriminator/layout contract here so even such a narrow
476        // verifier cannot mint a capability from a handle invalidated by CPI.
477        self.revalidate()?;
478        let proof = P::verify(self)?;
479        Ok(ExternalChecked {
480            account: self,
481            proof,
482            _marker: PhantomData,
483        })
484    }
485
486    /// Require a specific owner in fluent external-account code.
487    #[inline]
488    pub fn require_owner(&self, owner: &Address) -> Result<&Self, ProgramError> {
489        self.inner.check_owned_by(owner)?;
490        Ok(self)
491    }
492
493    /// Borrow a checked offset lens into this external account's bytes.
494    #[inline]
495    pub fn lens<V: ExternalLensValue, const OFFSET: usize>(
496        &self,
497    ) -> Result<ExternalLens<'info, V, OFFSET>, ProgramError> {
498        ExternalLens::new(self.data()?)
499    }
500
501    /// Hash the external account bytes for CPI/oracle consistency checks.
502    #[inline]
503    pub fn snapshot_hash(&self) -> Result<Sha256Hash, ProgramError> {
504        let data = self.data()?;
505        sha256_single(&data)
506    }
507
508    /// Verify the external account bytes still match a previous snapshot.
509    #[inline]
510    pub fn assert_snapshot(&self, expected: &Sha256Hash) -> ProgramResult {
511        if &self.snapshot_hash()? == expected {
512            Ok(())
513        } else {
514            Err(ProgramError::InvalidAccountData)
515        }
516    }
517
518    /// Run a closure and verify this external account is unchanged afterward.
519    #[inline]
520    pub fn assert_unchanged_after<R, F>(&self, f: F) -> Result<R, ProgramError>
521    where
522        F: FnOnce() -> Result<R, ProgramError>,
523    {
524        let before = self.snapshot_hash()?;
525        let result = f()?;
526        self.assert_snapshot(&before)?;
527        Ok(result)
528    }
529}
530
531impl<'info, T> ExternalAccount<'info, T>
532where
533    T: ExplainExternal,
534{
535    /// Emit structured external explain fields through the supplied sink.
536    #[inline]
537    pub fn explain<S: ExternalExplainSink>(&self, sink: &mut S) -> ProgramResult {
538        self.revalidate()?;
539        T::explain(self.inner, sink)
540    }
541}
542
543impl<'info, T> ExternalAccount<'info, T>
544where
545    T: ExternalZeroCopy + ExternalResolve,
546{
547    /// Resolve this external account into an owner-selected view family.
548    #[inline]
549    pub fn resolve(&self) -> Result<T::Resolved<'info>, ProgramError> {
550        self.revalidate()?;
551        T::resolve(self.inner)
552    }
553}
554
555impl<'info, T: ExternalZeroCopy> core::ops::Deref for ExternalAccount<'info, T> {
556    type Target = AccountView<'info>;
557
558    #[inline(always)]
559    fn deref(&self) -> &AccountView<'info> {
560        self.inner
561    }
562}
563
564/// Opaque witness to a foreign program's layout ABI.
565///
566/// Callers construct this once per foreign program they want to read
567/// from, typically as a `const` from build-time-embedded metadata or
568/// from the foreign program's Hopper manifest account.
569#[derive(Clone, Debug, PartialEq, Eq)]
570pub struct ForeignManifest {
571    /// Owner program that authored the layout. The account's owner
572    /// must match this address exactly.
573    pub program_id: Address,
574    /// Discriminator byte the foreign layout expects.
575    pub expected_disc: u8,
576    /// Canonical wire-fingerprint hash from the foreign program's
577    /// schema manifest. Matches `AccountLayout::WIRE_FINGERPRINT` on
578    /// the reader side.
579    pub expected_wire_fp: u64,
580    /// Inclusive range of `schema_epoch` values the reader supports.
581    /// Accounts outside this range fail verification. the caller can
582    /// then fall back to a migration path or a different manifest.
583    pub supported_epochs: core::ops::RangeInclusive<u32>,
584}
585
586impl ForeignManifest {
587    /// Build a single-epoch manifest covering `expected_wire_fp` for
588    /// `program_id` at exactly the given schema epoch.
589    pub const fn single_epoch(
590        program_id: Address,
591        expected_disc: u8,
592        expected_wire_fp: u64,
593        epoch: u32,
594    ) -> Self {
595        Self {
596            program_id,
597            expected_disc,
598            expected_wire_fp,
599            supported_epochs: epoch..=epoch,
600        }
601    }
602}
603
604/// A verified read-only handle into a foreign account.
605///
606/// `ForeignLens<'a, T>` borrows the underlying account data for its
607/// lifetime. Field access (`.get()`, `.field::<F, OFFSET>()`) performs
608/// only pointer arithmetic. no further verification, because all
609/// cross-program invariants were pinned at construction.
610pub struct ForeignLens<'a, T: AccountLayout + LayoutContract> {
611    inner: Ref<'a, T>,
612}
613
614impl<'a, T: AccountLayout + LayoutContract> ForeignLens<'a, T> {
615    /// Verify a foreign account against the supplied manifest and, on
616    /// success, return a read-only lens into its body.
617    ///
618    /// The four verification steps correspond one-to-one with the
619    /// checked-lens requirements:
620    ///
621    /// 1. owner match
622    /// 2. discriminator match (both `T::DISC` *and* `manifest.expected_disc`)
623    /// 3. wire-fingerprint match
624    /// 4. schema_epoch in supported range
625    #[inline]
626    pub fn open(
627        account: &'a AccountView<'a>,
628        manifest: &ForeignManifest,
629    ) -> Result<Self, ProgramError> {
630        // 1. Owner match. `check_owned_by` compares address bytes.
631        account.check_owned_by(&manifest.program_id)?;
632
633        // 2-4. Header inspection. must happen behind a byte borrow
634        //     so the data can't mutate underneath us. We use the same
635        //     load path authored accounts use, which verifies the
636        //     discriminator too. That closes #2.
637        let loaded: Ref<'a, T> = account.load::<T>()?;
638        if <T as AccountLayout>::DISC != manifest.expected_disc {
639            return Err(ProgramError::InvalidAccountData);
640        }
641
642        // Re-read the header bytes directly so we can match the
643        // manifest's wire-fingerprint and epoch fields. The load
644        // above already verified disc/version, so this step only
645        // checks the manifest-specific fields. HopperHeader is
646        // `#[repr(C, packed)]` at 16 bytes. `from_bytes` returns a
647        // properly bounds-checked reference without touching unaligned
648        // primitives (we copy packed fields out by value below).
649        let data = account.try_borrow()?;
650        let header = HopperHeader::from_bytes(&data).ok_or(ProgramError::AccountDataTooSmall)?;
651        // Packed-field reads must go through a local copy.
652        let layout_id = header.layout_id;
653        let schema_epoch = header.schema_epoch;
654        let actual_wire_fp = u64::from_le_bytes(layout_id);
655        if actual_wire_fp != manifest.expected_wire_fp {
656            return Err(ProgramError::InvalidAccountData);
657        }
658        if actual_wire_fp != <T as AccountLayout>::WIRE_FINGERPRINT {
659            return Err(ProgramError::InvalidAccountData);
660        }
661        if !manifest.supported_epochs.contains(&schema_epoch) {
662            return Err(ProgramError::InvalidAccountData);
663        }
664
665        // Explicit drop so the re-borrow guard releases before we
666        // hand out `loaded`, which already pins its own guard.
667        drop(data);
668
669        Ok(Self { inner: loaded })
670    }
671
672    /// The full verified layout. Field access through this path is
673    /// zero-cost; no further checks fire.
674    #[inline(always)]
675    pub fn get(&self) -> &T {
676        &self.inner
677    }
678
679    /// Project a typed field by byte offset. Returns a pointer-cast
680    /// reference with the lens's lifetime.
681    ///
682    /// `OFFSET` must be the field's offset *within the layout body*
683    /// (i.e. already past the 16-byte Hopper header). Callers should
684    /// prefer the auto-emitted `{FIELD}_OFFSET` constants from
685    /// `#[hopper::state]`.
686    #[inline(always)]
687    pub fn field<F: ZeroCopy, const OFFSET: usize>(&self) -> Result<&F, ProgramError> {
688        let body_size = core::mem::size_of::<T>();
689        let field_size = core::mem::size_of::<F>();
690        if OFFSET
691            .checked_add(field_size)
692            .map(|end| end > body_size)
693            .unwrap_or(true)
694        {
695            return Err(ProgramError::AccountDataTooSmall);
696        }
697        // SAFETY: We checked the byte range lies entirely inside the
698        // body. The layout is `Pod` (from `T: AccountLayout: ZeroCopy`),
699        // so every byte pattern is valid for `F: ZeroCopy`. The
700        // returned reference inherits the lens's lifetime and thus
701        // cannot outlive the underlying borrow guard.
702        // `Ref<T>` derefs to `T`; the `&T` annotation drives the coercion.
703        let layout_ref: &T = &self.inner;
704        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
705        unsafe {
706            let base = layout_ref as *const T as *const u8;
707            let field_ptr = base.add(OFFSET) as *const F;
708            Ok(&*field_ptr)
709        }
710    }
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716    use hopper_native::{
717        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
718    };
719    const EXTERNAL_OWNER: Address = Address::new_from_array([7; 32]);
720    struct SampleExternal;
721    impl ExternalZeroCopy for SampleExternal {
722        type View<'a> = SampleExternalView<'a>;
723
724        const OWNER: Option<Address> = Some(EXTERNAL_OWNER);
725        const DISCRIMINATOR: Option<&'static [u8]> = Some(b"PX");
726        const MIN_LEN: usize = 4;
727
728        fn view<'a>(data: Ref<'a, [u8]>) -> Result<Self::View<'a>, ProgramError> {
729            Ok(SampleExternalView { data })
730        }
731    }
732    struct SampleExternalView<'a> {
733        data: Ref<'a, [u8]>,
734    }
735    impl SampleExternalView<'_> {
736        fn tag(&self) -> &[u8] {
737            &self.data[..2]
738        }
739
740        fn value(&self) -> u16 {
741            u16::from_le_bytes([self.data[2], self.data[3]])
742        }
743    }
744    enum SampleResolved<'a> {
745        Price(SampleExternalView<'a>),
746    }
747    impl ExternalResolve for SampleExternal {
748        type Resolved<'a> = SampleResolved<'a>;
749
750        fn resolve<'a>(view: &'a AccountView<'a>) -> Result<Self::Resolved<'a>, ProgramError> {
751            Ok(SampleResolved::Price(
752                ExternalAccount::<SampleExternal>::try_new(view)?.view()?,
753            ))
754        }
755    }
756    struct SampleValueProof;
757    struct BlindProof;
758    struct SampleValueChecked {
759        value: u16,
760    }
761    impl ExternalProof<SampleExternal> for BlindProof {
762        type Proof<'a> = ();
763
764        fn verify<'a>(
765            _account: ExternalAccount<'a, SampleExternal>,
766        ) -> Result<Self::Proof<'a>, ProgramError> {
767            Ok(())
768        }
769    }
770    impl ExternalProof<SampleExternal> for SampleValueProof {
771        type Proof<'a> = SampleValueChecked;
772
773        fn verify<'a>(
774            account: ExternalAccount<'a, SampleExternal>,
775        ) -> Result<Self::Proof<'a>, ProgramError> {
776            let value = account.view()?.value();
777            if value == view_u16(b"12") {
778                Ok(SampleValueChecked { value })
779            } else {
780                Err(ProgramError::InvalidAccountData)
781            }
782        }
783    }
784    impl ExplainExternal for SampleExternal {
785        fn explain<S: ExternalExplainSink>(
786            account: &AccountView<'_>,
787            sink: &mut S,
788        ) -> ProgramResult {
789            let external = ExternalAccount::<SampleExternal>::try_new(account)?;
790            external.with_view(|view| {
791                sink.field_str("adapter", "SampleExternal")?;
792                sink.field_u64("value", view.value() as u64)
793            })
794        }
795    }
796    #[derive(Default)]
797    struct CountingExplainSink {
798        fields: usize,
799    }
800    impl ExternalExplainSink for CountingExplainSink {
801        fn field_str(&mut self, _name: &'static str, _value: &str) -> ProgramResult {
802            self.fields += 1;
803            Ok(())
804        }
805
806        fn field_u64(&mut self, _name: &'static str, _value: u64) -> ProgramResult {
807            self.fields += 1;
808            Ok(())
809        }
810    }
811    fn make_external_account(
812        owner: Address,
813        data: &[u8],
814    ) -> (std::vec::Vec<u64>, AccountView<'static>) {
815        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + data.len()).div_ceil(8)];
816        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
817        // SAFETY: Test helper initializes a valid RuntimeAccount header and
818        // copies `data` into the owned backing buffer at the payload offset.
819        unsafe {
820            raw.write(RuntimeAccount {
821                borrow_state: NOT_BORROWED,
822                is_signer: 0,
823                is_writable: 0,
824                executable: 0,
825                resize_delta: 0,
826                address: NativeAddress::new_from_array([3; 32]),
827                owner: NativeAddress::new_from_array(owner.to_bytes()),
828                lamports: 1,
829                data_len: data.len() as u64,
830            });
831            let data_ptr = (backing.as_mut_ptr() as *mut u8).add(RuntimeAccount::SIZE);
832            core::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, data.len());
833        }
834        // SAFETY: `raw` points at the RuntimeAccount header initialized above.
835        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
836        (backing, AccountView::from_backend(backend))
837    }
838
839    #[test]
840    fn manifest_single_epoch_is_inclusive_single_value() {
841        let program = Address::new_from_array([7u8; 32]);
842        let m = ForeignManifest::single_epoch(program, 42, 0xDEAD_BEEF_1234_5678, 3);
843        assert!(m.supported_epochs.contains(&3));
844        assert!(!m.supported_epochs.contains(&2));
845        assert!(!m.supported_epochs.contains(&4));
846        assert_eq!(m.expected_disc, 42);
847        assert_eq!(m.expected_wire_fp, 0xDEAD_BEEF_1234_5678);
848    }
849
850    #[test]
851    fn manifest_range_spans_inclusive() {
852        let program = Address::new_from_array([0u8; 32]);
853        let m = ForeignManifest {
854            program_id: program,
855            expected_disc: 1,
856            expected_wire_fp: 0,
857            supported_epochs: 2..=5,
858        };
859        for ok in [2u32, 3, 4, 5] {
860            assert!(m.supported_epochs.contains(&ok), "{ok}");
861        }
862        for fail in [0u32, 1, 6, 100] {
863            assert!(!m.supported_epochs.contains(&fail), "{fail}");
864        }
865    }
866    #[test]
867    fn external_account_validates_owner_discriminator_and_length() {
868        let (_backing, account) = make_external_account(EXTERNAL_OWNER, b"PX12");
869        let external = ExternalAccount::<SampleExternal>::try_new(&account).unwrap();
870        assert_eq!(external.owner(), EXTERNAL_OWNER);
871        assert_eq!(external.data_len(), 4);
872        external
873            .with_data(|data| {
874                assert_eq!(data, b"PX12");
875                Ok(())
876            })
877            .unwrap();
878        external
879            .with_view(|view| {
880                assert_eq!(view.tag(), b"PX");
881                assert_eq!(view.value(), u16::from_le_bytes(*b"12"));
882                Ok(())
883            })
884            .unwrap();
885        assert_eq!(external.lens::<u16, 2>().unwrap().get(), view_u16(b"12"));
886        let snapshot = external.snapshot_hash().unwrap();
887        external.assert_snapshot(&snapshot).unwrap();
888        let resolved = external.resolve().unwrap();
889        match resolved {
890            SampleResolved::Price(view) => assert_eq!(view.value(), view_u16(b"12")),
891        }
892        let checked = external.checked::<SampleValueProof>().unwrap();
893        assert_eq!(checked.proof().value, view_u16(b"12"));
894        let mut sink = CountingExplainSink::default();
895        external.explain(&mut sink).unwrap();
896        assert_eq!(sink.fields, 2);
897    }
898    fn view_u16(bytes: &[u8; 2]) -> u16 {
899        u16::from_le_bytes(*bytes)
900    }
901    #[test]
902    fn external_account_rejects_wrong_owner_or_prefix() {
903        let (_wrong_owner_backing, wrong_owner) =
904            make_external_account(Address::new_from_array([8; 32]), b"PX12");
905        assert_eq!(
906            ExternalAccount::<SampleExternal>::try_new(&wrong_owner).unwrap_err(),
907            ProgramError::IncorrectProgramId
908        );
909
910        let (_wrong_prefix_backing, wrong_prefix) = make_external_account(EXTERNAL_OWNER, b"NO12");
911        assert_eq!(
912            ExternalAccount::<SampleExternal>::try_new(&wrong_prefix).unwrap_err(),
913            ProgramError::InvalidAccountData
914        );
915
916        let (_short_backing, short) = make_external_account(EXTERNAL_OWNER, b"PX");
917        assert_eq!(
918            ExternalAccount::<SampleExternal>::try_new(&short).unwrap_err(),
919            ProgramError::AccountDataTooSmall
920        );
921    }
922
923    #[test]
924    fn external_account_revalidates_owner_and_discriminator_after_binding() {
925        let (_owner_backing, owner_changed) = make_external_account(EXTERNAL_OWNER, b"PX12");
926        let external = ExternalAccount::<SampleExternal>::try_new(&owner_changed).unwrap();
927        let checked = external.checked::<BlindProof>().unwrap();
928        // SAFETY: this test owns the synthetic account and models its original
929        // owner reassigning it during a writable CPI.
930        unsafe {
931            owner_changed.assign(&Address::new_from_array([8; 32]));
932        }
933        assert!(matches!(
934            external.view(),
935            Err(ProgramError::IncorrectProgramId)
936        ));
937        assert!(matches!(
938            checked.view(),
939            Err(ProgramError::IncorrectProgramId)
940        ));
941        assert!(matches!(
942            external.checked::<BlindProof>(),
943            Err(ProgramError::IncorrectProgramId)
944        ));
945
946        let (_disc_backing, discriminator_changed) = make_external_account(EXTERNAL_OWNER, b"PX12");
947        let external = ExternalAccount::<SampleExternal>::try_new(&discriminator_changed).unwrap();
948        let checked = external.checked::<BlindProof>().unwrap();
949        {
950            let mut data = discriminator_changed.try_borrow_mut().unwrap();
951            data[..2].copy_from_slice(b"NO");
952        }
953        assert!(matches!(
954            external.view(),
955            Err(ProgramError::InvalidAccountData)
956        ));
957        assert!(matches!(
958            checked.view(),
959            Err(ProgramError::InvalidAccountData)
960        ));
961        assert!(matches!(
962            external.checked::<BlindProof>(),
963            Err(ProgramError::InvalidAccountData)
964        ));
965    }
966}