use crate::account::AccountView;
use crate::address::Address;
use crate::borrow::Ref;
use crate::crypto::{sha256_single, Sha256Hash};
use crate::error::ProgramError;
use crate::layout::{HopperHeader, LayoutContract};
use crate::zerocopy::{AccountLayout, ZeroCopy};
use crate::ProgramResult;
use core::marker::PhantomData;
pub trait ExternalZeroCopy {
type View<'a>;
const OWNER: Option<Address> = None;
const DISCRIMINATOR: Option<&'static [u8]> = None;
const MIN_LEN: usize = 0;
#[inline]
fn validate(view: &AccountView<'_>) -> ProgramResult {
if let Some(owner) = Self::OWNER {
view.check_owned_by(&owner)?;
}
if view.data_len() < Self::MIN_LEN {
return Err(ProgramError::AccountDataTooSmall);
}
if let Some(discriminator) = Self::DISCRIMINATOR {
let data = view.try_borrow()?;
if data.len() < discriminator.len() {
return Err(ProgramError::AccountDataTooSmall);
}
if !data.starts_with(discriminator) {
return Err(ProgramError::InvalidAccountData);
}
}
Ok(())
}
fn view<'a>(data: Ref<'a, [u8]>) -> Result<Self::View<'a>, ProgramError>;
}
pub struct ExternalBytes<'a> {
data: Ref<'a, [u8]>,
}
impl<'a> ExternalBytes<'a> {
#[inline(always)]
pub const fn new(data: Ref<'a, [u8]>) -> Self {
Self { data }
}
#[inline(always)]
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
}
impl core::ops::Deref for ExternalBytes<'_> {
type Target = [u8];
#[inline(always)]
fn deref(&self) -> &[u8] {
self.as_bytes()
}
}
pub trait ExternalResolve {
type Resolved<'a>;
fn resolve<'a>(view: &'a AccountView<'a>) -> Result<Self::Resolved<'a>, ProgramError>;
}
pub trait ExternalProof<T: ExternalZeroCopy> {
type Proof<'a>;
fn verify<'a>(account: ExternalAccount<'a, T>) -> Result<Self::Proof<'a>, ProgramError>;
}
pub struct ExternalChecked<'info, T, P>
where
T: ExternalZeroCopy,
P: ExternalProof<T>,
{
account: ExternalAccount<'info, T>,
proof: P::Proof<'info>,
_marker: PhantomData<P>,
}
impl<'info, T, P> ExternalChecked<'info, T, P>
where
T: ExternalZeroCopy,
P: ExternalProof<T>,
{
#[inline(always)]
pub const fn account(&self) -> ExternalAccount<'info, T> {
self.account
}
#[inline(always)]
pub const fn proof(&self) -> &P::Proof<'info> {
&self.proof
}
#[inline]
pub fn view(&self) -> Result<T::View<'info>, ProgramError> {
self.account.view()
}
}
pub trait ExternalExplainSink {
fn field_str(&mut self, name: &'static str, value: &str) -> ProgramResult {
let _ = (name, value);
Ok(())
}
fn field_bytes(&mut self, name: &'static str, value: &[u8]) -> ProgramResult {
let _ = (name, value);
Ok(())
}
fn field_address(&mut self, name: &'static str, value: &Address) -> ProgramResult {
let _ = (name, value);
Ok(())
}
fn field_u64(&mut self, name: &'static str, value: u64) -> ProgramResult {
let _ = (name, value);
Ok(())
}
fn field_i64(&mut self, name: &'static str, value: i64) -> ProgramResult {
let _ = (name, value);
Ok(())
}
fn field_bool(&mut self, name: &'static str, value: bool) -> ProgramResult {
let _ = (name, value);
Ok(())
}
}
pub trait ExplainExternal: ExternalZeroCopy {
fn explain<S: ExternalExplainSink>(account: &AccountView<'_>, sink: &mut S) -> ProgramResult;
}
pub trait ExternalLensValue: Sized {
const SIZE: usize;
fn read(bytes: &[u8]) -> Self;
}
macro_rules! impl_external_lens_value_le {
($ty:ty, $size:expr) => {
impl ExternalLensValue for $ty {
const SIZE: usize = $size;
#[inline(always)]
fn read(bytes: &[u8]) -> Self {
let mut raw = [0u8; $size];
raw.copy_from_slice(bytes);
<$ty>::from_le_bytes(raw)
}
}
};
}
impl ExternalLensValue for u8 {
const SIZE: usize = 1;
#[inline(always)]
fn read(bytes: &[u8]) -> Self {
bytes[0]
}
}
impl ExternalLensValue for i8 {
const SIZE: usize = 1;
#[inline(always)]
fn read(bytes: &[u8]) -> Self {
bytes[0] as i8
}
}
impl_external_lens_value_le!(u16, 2);
impl_external_lens_value_le!(u32, 4);
impl_external_lens_value_le!(u64, 8);
impl_external_lens_value_le!(u128, 16);
impl_external_lens_value_le!(i16, 2);
impl_external_lens_value_le!(i32, 4);
impl_external_lens_value_le!(i64, 8);
impl_external_lens_value_le!(i128, 16);
impl<const N: usize> ExternalLensValue for [u8; N] {
const SIZE: usize = N;
#[inline(always)]
fn read(bytes: &[u8]) -> Self {
let mut raw = [0u8; N];
raw.copy_from_slice(bytes);
raw
}
}
impl ExternalLensValue for Address {
const SIZE: usize = 32;
#[inline(always)]
fn read(bytes: &[u8]) -> Self {
let mut raw = [0u8; 32];
raw.copy_from_slice(bytes);
Address::new_from_array(raw)
}
}
pub struct ExternalLens<'a, V: ExternalLensValue, const OFFSET: usize> {
data: Ref<'a, [u8]>,
_value: PhantomData<V>,
}
impl<'a, V: ExternalLensValue, const OFFSET: usize> ExternalLens<'a, V, OFFSET> {
#[inline]
fn new(data: Ref<'a, [u8]>) -> Result<Self, ProgramError> {
let end = OFFSET
.checked_add(V::SIZE)
.ok_or(ProgramError::ArithmeticOverflow)?;
if end > data.len() {
return Err(ProgramError::AccountDataTooSmall);
}
Ok(Self {
data,
_value: PhantomData,
})
}
#[inline(always)]
pub fn as_bytes(&self) -> &[u8] {
&self.data[OFFSET..OFFSET + V::SIZE]
}
#[inline(always)]
pub fn get(&self) -> V {
V::read(self.as_bytes())
}
}
#[repr(transparent)]
pub struct ExternalAccount<'info, T: ExternalZeroCopy> {
inner: &'info AccountView<'info>,
_ty: PhantomData<T>,
}
impl<'info, T: ExternalZeroCopy> Clone for ExternalAccount<'info, T> {
fn clone(&self) -> Self {
*self
}
}
impl<'info, T: ExternalZeroCopy> Copy for ExternalAccount<'info, T> {}
impl<T: ExternalZeroCopy> core::fmt::Debug for ExternalAccount<'_, T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ExternalAccount")
.field("key", self.key())
.field("owner", &self.owner())
.field("data_len", &self.data_len())
.finish()
}
}
impl<'info, T: ExternalZeroCopy> ExternalAccount<'info, T> {
#[inline(always)]
fn revalidate(&self) -> ProgramResult {
T::validate(self.inner)
}
#[inline(always)]
pub unsafe fn new_unchecked(view: &'info AccountView<'info>) -> Self {
Self {
inner: view,
_ty: PhantomData,
}
}
#[inline]
pub fn try_new(view: &'info AccountView<'info>) -> Result<Self, ProgramError> {
T::validate(view)?;
Ok(Self {
inner: view,
_ty: PhantomData,
})
}
#[inline(always)]
pub fn as_account(&self) -> &'info AccountView<'info> {
self.inner
}
#[inline(always)]
pub fn key(&self) -> &Address {
self.inner.address()
}
#[inline(always)]
pub fn owner(&self) -> Address {
self.inner.read_owner()
}
#[inline(always)]
pub fn data_len(&self) -> usize {
self.inner.data_len()
}
#[inline(always)]
pub fn data(&self) -> Result<Ref<'info, [u8]>, ProgramError> {
self.revalidate()?;
self.inner.try_borrow()
}
#[inline]
pub fn with_data<R, F>(&self, f: F) -> Result<R, ProgramError>
where
F: FnOnce(&[u8]) -> Result<R, ProgramError>,
{
let data = self.data()?;
f(&data)
}
#[inline]
pub fn view(&self) -> Result<T::View<'info>, ProgramError> {
T::view(self.data()?)
}
#[inline]
pub fn with_view<R, F>(&self, f: F) -> Result<R, ProgramError>
where
F: FnOnce(T::View<'info>) -> Result<R, ProgramError>,
{
f(self.view()?)
}
#[inline]
pub fn checked<P>(self) -> Result<ExternalChecked<'info, T, P>, ProgramError>
where
P: ExternalProof<T>,
{
self.revalidate()?;
let proof = P::verify(self)?;
Ok(ExternalChecked {
account: self,
proof,
_marker: PhantomData,
})
}
#[inline]
pub fn require_owner(&self, owner: &Address) -> Result<&Self, ProgramError> {
self.inner.check_owned_by(owner)?;
Ok(self)
}
#[inline]
pub fn lens<V: ExternalLensValue, const OFFSET: usize>(
&self,
) -> Result<ExternalLens<'info, V, OFFSET>, ProgramError> {
ExternalLens::new(self.data()?)
}
#[inline]
pub fn snapshot_hash(&self) -> Result<Sha256Hash, ProgramError> {
let data = self.data()?;
sha256_single(&data)
}
#[inline]
pub fn assert_snapshot(&self, expected: &Sha256Hash) -> ProgramResult {
if &self.snapshot_hash()? == expected {
Ok(())
} else {
Err(ProgramError::InvalidAccountData)
}
}
#[inline]
pub fn assert_unchanged_after<R, F>(&self, f: F) -> Result<R, ProgramError>
where
F: FnOnce() -> Result<R, ProgramError>,
{
let before = self.snapshot_hash()?;
let result = f()?;
self.assert_snapshot(&before)?;
Ok(result)
}
}
impl<'info, T> ExternalAccount<'info, T>
where
T: ExplainExternal,
{
#[inline]
pub fn explain<S: ExternalExplainSink>(&self, sink: &mut S) -> ProgramResult {
self.revalidate()?;
T::explain(self.inner, sink)
}
}
impl<'info, T> ExternalAccount<'info, T>
where
T: ExternalZeroCopy + ExternalResolve,
{
#[inline]
pub fn resolve(&self) -> Result<T::Resolved<'info>, ProgramError> {
self.revalidate()?;
T::resolve(self.inner)
}
}
impl<'info, T: ExternalZeroCopy> core::ops::Deref for ExternalAccount<'info, T> {
type Target = AccountView<'info>;
#[inline(always)]
fn deref(&self) -> &AccountView<'info> {
self.inner
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ForeignManifest {
pub program_id: Address,
pub expected_disc: u8,
pub expected_wire_fp: u64,
pub supported_epochs: core::ops::RangeInclusive<u32>,
}
impl ForeignManifest {
pub const fn single_epoch(
program_id: Address,
expected_disc: u8,
expected_wire_fp: u64,
epoch: u32,
) -> Self {
Self {
program_id,
expected_disc,
expected_wire_fp,
supported_epochs: epoch..=epoch,
}
}
}
pub struct ForeignLens<'a, T: AccountLayout + LayoutContract> {
inner: Ref<'a, T>,
}
impl<'a, T: AccountLayout + LayoutContract> ForeignLens<'a, T> {
#[inline]
pub fn open(
account: &'a AccountView<'a>,
manifest: &ForeignManifest,
) -> Result<Self, ProgramError> {
account.check_owned_by(&manifest.program_id)?;
let loaded: Ref<'a, T> = account.load::<T>()?;
if <T as AccountLayout>::DISC != manifest.expected_disc {
return Err(ProgramError::InvalidAccountData);
}
let data = account.try_borrow()?;
let header = HopperHeader::from_bytes(&data).ok_or(ProgramError::AccountDataTooSmall)?;
let layout_id = header.layout_id;
let schema_epoch = header.schema_epoch;
let actual_wire_fp = u64::from_le_bytes(layout_id);
if actual_wire_fp != manifest.expected_wire_fp {
return Err(ProgramError::InvalidAccountData);
}
if actual_wire_fp != <T as AccountLayout>::WIRE_FINGERPRINT {
return Err(ProgramError::InvalidAccountData);
}
if !manifest.supported_epochs.contains(&schema_epoch) {
return Err(ProgramError::InvalidAccountData);
}
drop(data);
Ok(Self { inner: loaded })
}
#[inline(always)]
pub fn get(&self) -> &T {
&self.inner
}
#[inline(always)]
pub fn field<F: ZeroCopy, const OFFSET: usize>(&self) -> Result<&F, ProgramError> {
let body_size = core::mem::size_of::<T>();
let field_size = core::mem::size_of::<F>();
if OFFSET
.checked_add(field_size)
.map(|end| end > body_size)
.unwrap_or(true)
{
return Err(ProgramError::AccountDataTooSmall);
}
let layout_ref: &T = &self.inner;
unsafe {
let base = layout_ref as *const T as *const u8;
let field_ptr = base.add(OFFSET) as *const F;
Ok(&*field_ptr)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use hopper_native::{
AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
};
const EXTERNAL_OWNER: Address = Address::new_from_array([7; 32]);
struct SampleExternal;
impl ExternalZeroCopy for SampleExternal {
type View<'a> = SampleExternalView<'a>;
const OWNER: Option<Address> = Some(EXTERNAL_OWNER);
const DISCRIMINATOR: Option<&'static [u8]> = Some(b"PX");
const MIN_LEN: usize = 4;
fn view<'a>(data: Ref<'a, [u8]>) -> Result<Self::View<'a>, ProgramError> {
Ok(SampleExternalView { data })
}
}
struct SampleExternalView<'a> {
data: Ref<'a, [u8]>,
}
impl SampleExternalView<'_> {
fn tag(&self) -> &[u8] {
&self.data[..2]
}
fn value(&self) -> u16 {
u16::from_le_bytes([self.data[2], self.data[3]])
}
}
enum SampleResolved<'a> {
Price(SampleExternalView<'a>),
}
impl ExternalResolve for SampleExternal {
type Resolved<'a> = SampleResolved<'a>;
fn resolve<'a>(view: &'a AccountView<'a>) -> Result<Self::Resolved<'a>, ProgramError> {
Ok(SampleResolved::Price(
ExternalAccount::<SampleExternal>::try_new(view)?.view()?,
))
}
}
struct SampleValueProof;
struct BlindProof;
struct SampleValueChecked {
value: u16,
}
impl ExternalProof<SampleExternal> for BlindProof {
type Proof<'a> = ();
fn verify<'a>(
_account: ExternalAccount<'a, SampleExternal>,
) -> Result<Self::Proof<'a>, ProgramError> {
Ok(())
}
}
impl ExternalProof<SampleExternal> for SampleValueProof {
type Proof<'a> = SampleValueChecked;
fn verify<'a>(
account: ExternalAccount<'a, SampleExternal>,
) -> Result<Self::Proof<'a>, ProgramError> {
let value = account.view()?.value();
if value == view_u16(b"12") {
Ok(SampleValueChecked { value })
} else {
Err(ProgramError::InvalidAccountData)
}
}
}
impl ExplainExternal for SampleExternal {
fn explain<S: ExternalExplainSink>(
account: &AccountView<'_>,
sink: &mut S,
) -> ProgramResult {
let external = ExternalAccount::<SampleExternal>::try_new(account)?;
external.with_view(|view| {
sink.field_str("adapter", "SampleExternal")?;
sink.field_u64("value", view.value() as u64)
})
}
}
#[derive(Default)]
struct CountingExplainSink {
fields: usize,
}
impl ExternalExplainSink for CountingExplainSink {
fn field_str(&mut self, _name: &'static str, _value: &str) -> ProgramResult {
self.fields += 1;
Ok(())
}
fn field_u64(&mut self, _name: &'static str, _value: u64) -> ProgramResult {
self.fields += 1;
Ok(())
}
}
fn make_external_account(
owner: Address,
data: &[u8],
) -> (std::vec::Vec<u64>, AccountView<'static>) {
let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + data.len()).div_ceil(8)];
let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
unsafe {
raw.write(RuntimeAccount {
borrow_state: NOT_BORROWED,
is_signer: 0,
is_writable: 0,
executable: 0,
resize_delta: 0,
address: NativeAddress::new_from_array([3; 32]),
owner: NativeAddress::new_from_array(owner.to_bytes()),
lamports: 1,
data_len: data.len() as u64,
});
let data_ptr = (backing.as_mut_ptr() as *mut u8).add(RuntimeAccount::SIZE);
core::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, data.len());
}
let backend = unsafe { NativeAccountView::new_unchecked(raw) };
(backing, AccountView::from_backend(backend))
}
#[test]
fn manifest_single_epoch_is_inclusive_single_value() {
let program = Address::new_from_array([7u8; 32]);
let m = ForeignManifest::single_epoch(program, 42, 0xDEAD_BEEF_1234_5678, 3);
assert!(m.supported_epochs.contains(&3));
assert!(!m.supported_epochs.contains(&2));
assert!(!m.supported_epochs.contains(&4));
assert_eq!(m.expected_disc, 42);
assert_eq!(m.expected_wire_fp, 0xDEAD_BEEF_1234_5678);
}
#[test]
fn manifest_range_spans_inclusive() {
let program = Address::new_from_array([0u8; 32]);
let m = ForeignManifest {
program_id: program,
expected_disc: 1,
expected_wire_fp: 0,
supported_epochs: 2..=5,
};
for ok in [2u32, 3, 4, 5] {
assert!(m.supported_epochs.contains(&ok), "{ok}");
}
for fail in [0u32, 1, 6, 100] {
assert!(!m.supported_epochs.contains(&fail), "{fail}");
}
}
#[test]
fn external_account_validates_owner_discriminator_and_length() {
let (_backing, account) = make_external_account(EXTERNAL_OWNER, b"PX12");
let external = ExternalAccount::<SampleExternal>::try_new(&account).unwrap();
assert_eq!(external.owner(), EXTERNAL_OWNER);
assert_eq!(external.data_len(), 4);
external
.with_data(|data| {
assert_eq!(data, b"PX12");
Ok(())
})
.unwrap();
external
.with_view(|view| {
assert_eq!(view.tag(), b"PX");
assert_eq!(view.value(), u16::from_le_bytes(*b"12"));
Ok(())
})
.unwrap();
assert_eq!(external.lens::<u16, 2>().unwrap().get(), view_u16(b"12"));
let snapshot = external.snapshot_hash().unwrap();
external.assert_snapshot(&snapshot).unwrap();
let resolved = external.resolve().unwrap();
match resolved {
SampleResolved::Price(view) => assert_eq!(view.value(), view_u16(b"12")),
}
let checked = external.checked::<SampleValueProof>().unwrap();
assert_eq!(checked.proof().value, view_u16(b"12"));
let mut sink = CountingExplainSink::default();
external.explain(&mut sink).unwrap();
assert_eq!(sink.fields, 2);
}
fn view_u16(bytes: &[u8; 2]) -> u16 {
u16::from_le_bytes(*bytes)
}
#[test]
fn external_account_rejects_wrong_owner_or_prefix() {
let (_wrong_owner_backing, wrong_owner) =
make_external_account(Address::new_from_array([8; 32]), b"PX12");
assert_eq!(
ExternalAccount::<SampleExternal>::try_new(&wrong_owner).unwrap_err(),
ProgramError::IncorrectProgramId
);
let (_wrong_prefix_backing, wrong_prefix) = make_external_account(EXTERNAL_OWNER, b"NO12");
assert_eq!(
ExternalAccount::<SampleExternal>::try_new(&wrong_prefix).unwrap_err(),
ProgramError::InvalidAccountData
);
let (_short_backing, short) = make_external_account(EXTERNAL_OWNER, b"PX");
assert_eq!(
ExternalAccount::<SampleExternal>::try_new(&short).unwrap_err(),
ProgramError::AccountDataTooSmall
);
}
#[test]
fn external_account_revalidates_owner_and_discriminator_after_binding() {
let (_owner_backing, owner_changed) = make_external_account(EXTERNAL_OWNER, b"PX12");
let external = ExternalAccount::<SampleExternal>::try_new(&owner_changed).unwrap();
let checked = external.checked::<BlindProof>().unwrap();
unsafe {
owner_changed.assign(&Address::new_from_array([8; 32]));
}
assert!(matches!(
external.view(),
Err(ProgramError::IncorrectProgramId)
));
assert!(matches!(
checked.view(),
Err(ProgramError::IncorrectProgramId)
));
assert!(matches!(
external.checked::<BlindProof>(),
Err(ProgramError::IncorrectProgramId)
));
let (_disc_backing, discriminator_changed) = make_external_account(EXTERNAL_OWNER, b"PX12");
let external = ExternalAccount::<SampleExternal>::try_new(&discriminator_changed).unwrap();
let checked = external.checked::<BlindProof>().unwrap();
{
let mut data = discriminator_changed.try_borrow_mut().unwrap();
data[..2].copy_from_slice(b"NO");
}
assert!(matches!(
external.view(),
Err(ProgramError::InvalidAccountData)
));
assert!(matches!(
checked.view(),
Err(ProgramError::InvalidAccountData)
));
assert!(matches!(
external.checked::<BlindProof>(),
Err(ProgramError::InvalidAccountData)
));
}
}