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: `OFFSET + size_of::<F>() <= size_of::<T>()` was checked
705        // above, so the field lies inside the layout the guard covers; `F:
706        // ZeroCopy` has alignment 1 and accepts every bit pattern.
707        unsafe {
708            let base = layout_ref as *const T as *const u8;
709            let field_ptr = base.add(OFFSET) as *const F;
710            Ok(&*field_ptr)
711        }
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718    use hopper_native::{
719        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
720    };
721    const EXTERNAL_OWNER: Address = Address::new_from_array([7; 32]);
722    struct SampleExternal;
723    impl ExternalZeroCopy for SampleExternal {
724        type View<'a> = SampleExternalView<'a>;
725
726        const OWNER: Option<Address> = Some(EXTERNAL_OWNER);
727        const DISCRIMINATOR: Option<&'static [u8]> = Some(b"PX");
728        const MIN_LEN: usize = 4;
729
730        fn view<'a>(data: Ref<'a, [u8]>) -> Result<Self::View<'a>, ProgramError> {
731            Ok(SampleExternalView { data })
732        }
733    }
734    struct SampleExternalView<'a> {
735        data: Ref<'a, [u8]>,
736    }
737    impl SampleExternalView<'_> {
738        fn tag(&self) -> &[u8] {
739            &self.data[..2]
740        }
741
742        fn value(&self) -> u16 {
743            u16::from_le_bytes([self.data[2], self.data[3]])
744        }
745    }
746    enum SampleResolved<'a> {
747        Price(SampleExternalView<'a>),
748    }
749    impl ExternalResolve for SampleExternal {
750        type Resolved<'a> = SampleResolved<'a>;
751
752        fn resolve<'a>(view: &'a AccountView<'a>) -> Result<Self::Resolved<'a>, ProgramError> {
753            Ok(SampleResolved::Price(
754                ExternalAccount::<SampleExternal>::try_new(view)?.view()?,
755            ))
756        }
757    }
758    struct SampleValueProof;
759    struct BlindProof;
760    struct SampleValueChecked {
761        value: u16,
762    }
763    impl ExternalProof<SampleExternal> for BlindProof {
764        type Proof<'a> = ();
765
766        fn verify<'a>(
767            _account: ExternalAccount<'a, SampleExternal>,
768        ) -> Result<Self::Proof<'a>, ProgramError> {
769            Ok(())
770        }
771    }
772    impl ExternalProof<SampleExternal> for SampleValueProof {
773        type Proof<'a> = SampleValueChecked;
774
775        fn verify<'a>(
776            account: ExternalAccount<'a, SampleExternal>,
777        ) -> Result<Self::Proof<'a>, ProgramError> {
778            let value = account.view()?.value();
779            if value == view_u16(b"12") {
780                Ok(SampleValueChecked { value })
781            } else {
782                Err(ProgramError::InvalidAccountData)
783            }
784        }
785    }
786    impl ExplainExternal for SampleExternal {
787        fn explain<S: ExternalExplainSink>(
788            account: &AccountView<'_>,
789            sink: &mut S,
790        ) -> ProgramResult {
791            let external = ExternalAccount::<SampleExternal>::try_new(account)?;
792            external.with_view(|view| {
793                sink.field_str("adapter", "SampleExternal")?;
794                sink.field_u64("value", view.value() as u64)
795            })
796        }
797    }
798    #[derive(Default)]
799    struct CountingExplainSink {
800        fields: usize,
801    }
802    impl ExternalExplainSink for CountingExplainSink {
803        fn field_str(&mut self, _name: &'static str, _value: &str) -> ProgramResult {
804            self.fields += 1;
805            Ok(())
806        }
807
808        fn field_u64(&mut self, _name: &'static str, _value: u64) -> ProgramResult {
809            self.fields += 1;
810            Ok(())
811        }
812    }
813    fn make_external_account(
814        owner: Address,
815        data: &[u8],
816    ) -> (std::vec::Vec<u64>, AccountView<'static>) {
817        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + data.len()).div_ceil(8)];
818        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
819        // SAFETY: Test helper initializes a valid RuntimeAccount header and
820        // copies `data` into the owned backing buffer at the payload offset.
821        unsafe {
822            raw.write(RuntimeAccount {
823                borrow_state: NOT_BORROWED,
824                is_signer: 0,
825                is_writable: 0,
826                executable: 0,
827                resize_delta: 0,
828                address: NativeAddress::new_from_array([3; 32]),
829                owner: NativeAddress::new_from_array(owner.to_bytes()),
830                lamports: 1,
831                data_len: data.len() as u64,
832            });
833            let data_ptr = (backing.as_mut_ptr() as *mut u8).add(RuntimeAccount::SIZE);
834            core::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, data.len());
835        }
836        // SAFETY: `raw` points at the RuntimeAccount header initialized above.
837        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
838        (backing, AccountView::from_backend(backend))
839    }
840
841    #[test]
842    fn manifest_single_epoch_is_inclusive_single_value() {
843        let program = Address::new_from_array([7u8; 32]);
844        let m = ForeignManifest::single_epoch(program, 42, 0xDEAD_BEEF_1234_5678, 3);
845        assert!(m.supported_epochs.contains(&3));
846        assert!(!m.supported_epochs.contains(&2));
847        assert!(!m.supported_epochs.contains(&4));
848        assert_eq!(m.expected_disc, 42);
849        assert_eq!(m.expected_wire_fp, 0xDEAD_BEEF_1234_5678);
850    }
851
852    #[test]
853    fn manifest_range_spans_inclusive() {
854        let program = Address::new_from_array([0u8; 32]);
855        let m = ForeignManifest {
856            program_id: program,
857            expected_disc: 1,
858            expected_wire_fp: 0,
859            supported_epochs: 2..=5,
860        };
861        for ok in [2u32, 3, 4, 5] {
862            assert!(m.supported_epochs.contains(&ok), "{ok}");
863        }
864        for fail in [0u32, 1, 6, 100] {
865            assert!(!m.supported_epochs.contains(&fail), "{fail}");
866        }
867    }
868    #[test]
869    fn external_account_validates_owner_discriminator_and_length() {
870        let (_backing, account) = make_external_account(EXTERNAL_OWNER, b"PX12");
871        let external = ExternalAccount::<SampleExternal>::try_new(&account).unwrap();
872        assert_eq!(external.owner(), EXTERNAL_OWNER);
873        assert_eq!(external.data_len(), 4);
874        external
875            .with_data(|data| {
876                assert_eq!(data, b"PX12");
877                Ok(())
878            })
879            .unwrap();
880        external
881            .with_view(|view| {
882                assert_eq!(view.tag(), b"PX");
883                assert_eq!(view.value(), u16::from_le_bytes(*b"12"));
884                Ok(())
885            })
886            .unwrap();
887        assert_eq!(external.lens::<u16, 2>().unwrap().get(), view_u16(b"12"));
888        let snapshot = external.snapshot_hash().unwrap();
889        external.assert_snapshot(&snapshot).unwrap();
890        let resolved = external.resolve().unwrap();
891        match resolved {
892            SampleResolved::Price(view) => assert_eq!(view.value(), view_u16(b"12")),
893        }
894        let checked = external.checked::<SampleValueProof>().unwrap();
895        assert_eq!(checked.proof().value, view_u16(b"12"));
896        let mut sink = CountingExplainSink::default();
897        external.explain(&mut sink).unwrap();
898        assert_eq!(sink.fields, 2);
899    }
900    fn view_u16(bytes: &[u8; 2]) -> u16 {
901        u16::from_le_bytes(*bytes)
902    }
903    #[test]
904    fn external_account_rejects_wrong_owner_or_prefix() {
905        let (_wrong_owner_backing, wrong_owner) =
906            make_external_account(Address::new_from_array([8; 32]), b"PX12");
907        assert_eq!(
908            ExternalAccount::<SampleExternal>::try_new(&wrong_owner).unwrap_err(),
909            ProgramError::IncorrectProgramId
910        );
911
912        let (_wrong_prefix_backing, wrong_prefix) = make_external_account(EXTERNAL_OWNER, b"NO12");
913        assert_eq!(
914            ExternalAccount::<SampleExternal>::try_new(&wrong_prefix).unwrap_err(),
915            ProgramError::InvalidAccountData
916        );
917
918        let (_short_backing, short) = make_external_account(EXTERNAL_OWNER, b"PX");
919        assert_eq!(
920            ExternalAccount::<SampleExternal>::try_new(&short).unwrap_err(),
921            ProgramError::AccountDataTooSmall
922        );
923    }
924
925    #[test]
926    fn external_account_revalidates_owner_and_discriminator_after_binding() {
927        let (_owner_backing, owner_changed) = make_external_account(EXTERNAL_OWNER, b"PX12");
928        let external = ExternalAccount::<SampleExternal>::try_new(&owner_changed).unwrap();
929        let checked = external.checked::<BlindProof>().unwrap();
930        // SAFETY: this test owns the synthetic account and models its original
931        // owner reassigning it during a writable CPI.
932        unsafe {
933            owner_changed.assign(&Address::new_from_array([8; 32]));
934        }
935        assert!(matches!(
936            external.view(),
937            Err(ProgramError::IncorrectProgramId)
938        ));
939        assert!(matches!(
940            checked.view(),
941            Err(ProgramError::IncorrectProgramId)
942        ));
943        assert!(matches!(
944            external.checked::<BlindProof>(),
945            Err(ProgramError::IncorrectProgramId)
946        ));
947
948        let (_disc_backing, discriminator_changed) = make_external_account(EXTERNAL_OWNER, b"PX12");
949        let external = ExternalAccount::<SampleExternal>::try_new(&discriminator_changed).unwrap();
950        let checked = external.checked::<BlindProof>().unwrap();
951        {
952            let mut data = discriminator_changed.try_borrow_mut().unwrap();
953            data[..2].copy_from_slice(b"NO");
954        }
955        assert!(matches!(
956            external.view(),
957            Err(ProgramError::InvalidAccountData)
958        ));
959        assert!(matches!(
960            checked.view(),
961            Err(ProgramError::InvalidAccountData)
962        ));
963        assert!(matches!(
964            external.checked::<BlindProof>(),
965            Err(ProgramError::InvalidAccountData)
966        ));
967    }
968}