use hopper_native::{
AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
};
use hopper_runtime::remaining::{RemainingAccounts, RemainingError, MAX_REMAINING_ACCOUNTS};
use hopper_runtime::segment_borrow::SegmentBorrowRegistry;
use hopper_runtime::write_policy::{write_policy_violation, WritePolicy, WriteRange};
use hopper_runtime::{
apply_pending_migrations, AccountAudit, AccountView, Address, FieldInfo, FieldMap,
HopperHeader, LayoutContract, LayoutMigration, MigrationEdge, ProgramError,
};
const DEFAULT_OWNER: [u8; 32] = [9; 32];
fn make_account(
address: [u8; 32],
owner: [u8; 32],
is_signer: bool,
is_writable: bool,
lamports: u64,
data: &[u8],
) -> (Vec<u64>, AccountView<'static>) {
let total = RuntimeAccount::SIZE + data.len();
let mut backing = vec![0u64; total.div_ceil(8)];
let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
unsafe {
raw.write(RuntimeAccount {
borrow_state: NOT_BORROWED,
is_signer: u8::from(is_signer),
is_writable: u8::from(is_writable),
executable: 0,
resize_delta: 0,
address: NativeAddress::new_from_array(address),
owner: NativeAddress::new_from_array(owner),
lamports,
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) };
let view = unsafe { core::mem::transmute::<NativeAccountView, AccountView>(backend) };
(backing, view)
}
fn make_distinct_accounts(count: usize) -> (Vec<Vec<u64>>, Vec<AccountView<'static>>) {
assert!(count < 256, "test helper uses one address byte per account");
let mut backings = Vec::with_capacity(count);
let mut views = Vec::with_capacity(count);
for i in 0..count {
let (backing, view) =
make_account([(i + 1) as u8; 32], DEFAULT_OWNER, false, false, 1, b"");
backings.push(backing);
views.push(view);
}
(backings, views)
}
#[test]
fn remaining_advertised_length_is_true_length_at_zero_and_nonzero() {
let empty: [AccountView<'static>; 0] = [];
for view in [
RemainingAccounts::strict(&empty, &empty),
RemainingAccounts::passthrough(&empty, &empty),
] {
assert_eq!(view.len(), 0);
assert!(view.is_empty());
assert_eq!(view.get(0).unwrap(), None);
assert!(view.iter().next().is_none());
assert_eq!(view.as_slice().len(), 0);
}
let (_backings, views) = make_distinct_accounts(5);
let strict = RemainingAccounts::strict(&[], &views);
assert_eq!(strict.len(), 5);
for (i, item) in strict.iter().enumerate() {
assert_eq!(item.unwrap().address(), views[i].address());
}
assert_eq!(strict.get(5).unwrap(), None);
}
#[test]
fn remaining_cap_is_exact_and_loud_at_the_64_boundary() {
assert_eq!(MAX_REMAINING_ACCOUNTS, 64);
let (_backings, views) = make_distinct_accounts(MAX_REMAINING_ACCOUNTS);
let strict = RemainingAccounts::strict(&[], &views);
let mut iter = strict.iter();
for expected in &views {
assert_eq!(iter.next().unwrap().unwrap().address(), expected.address());
}
assert!(iter.next().is_none());
let (_backings, views) = make_distinct_accounts(MAX_REMAINING_ACCOUNTS + 1);
for view in [
RemainingAccounts::strict(&[], &views),
RemainingAccounts::passthrough(&[], &views),
] {
assert_eq!(view.len(), MAX_REMAINING_ACCOUNTS + 1);
let mut iter = view.iter();
for _ in 0..MAX_REMAINING_ACCOUNTS {
assert!(iter.next().unwrap().is_ok());
}
assert_eq!(
iter.next().unwrap().unwrap_err(),
ProgramError::from(RemainingError::Overflow)
);
assert!(iter.next().is_none());
}
let strict = RemainingAccounts::strict(&[], &views);
assert!(strict.get(MAX_REMAINING_ACCOUNTS - 1).unwrap().is_some());
assert_eq!(
strict.get(MAX_REMAINING_ACCOUNTS).unwrap_err(),
ProgramError::from(RemainingError::Overflow)
);
}
#[test]
fn typed_remaining_sets_parse_exactly_what_they_advertise() {
let empty: [AccountView<'static>; 0] = [];
let views4 = RemainingAccounts::strict(&empty, &empty)
.account_views::<4>()
.unwrap();
assert_eq!(views4.len(), 0);
assert!(views4.is_empty());
assert!(views4.get(0).is_none());
let (_backings, views) = make_distinct_accounts(4);
let parsed = RemainingAccounts::strict(&[], &views)
.account_views::<4>()
.unwrap();
assert_eq!(parsed.len(), 4);
for (i, expected) in views.iter().enumerate() {
assert_eq!(parsed.get(i).unwrap().address(), expected.address());
}
assert!(parsed.get(4).is_none());
assert_eq!(parsed.iter().count(), 4);
let (_backings, views) = make_distinct_accounts(5);
assert_eq!(
RemainingAccounts::strict(&[], &views)
.account_views::<4>()
.err()
.unwrap(),
ProgramError::from(RemainingError::Overflow)
);
let mut backings = Vec::new();
let mut signer_views = Vec::new();
for i in 0..3usize {
let (backing, view) =
make_account([(i + 40) as u8; 32], DEFAULT_OWNER, true, false, 1, b"");
backings.push(backing);
signer_views.push(view);
}
let signers = RemainingAccounts::strict(&[], &signer_views)
.signers::<3>()
.unwrap();
assert_eq!(signers.len(), 3);
for (i, expected) in signer_views.iter().enumerate() {
assert_eq!(signers.get(i).unwrap().key(), expected.address());
}
assert!(signers.get(3).is_none());
assert_eq!(
RemainingAccounts::strict(&[], &signer_views)
.signers::<2>()
.err()
.unwrap(),
ProgramError::from(RemainingError::Overflow)
);
}
#[test]
fn typed_and_lazy_parsers_report_and_enforce_true_capacity() {
let (_backings, views) = make_distinct_accounts(3);
let accounts = RemainingAccounts::strict(&[], &views);
let mut typed = accounts.typed();
assert_eq!(typed.consumed(), 0);
assert_eq!(typed.remaining_len(), 3);
for (i, expected) in views.iter().enumerate() {
let account = typed.next_account().unwrap();
assert_eq!(account.address(), expected.address());
assert_eq!(typed.consumed(), i + 1);
assert_eq!(typed.remaining_len(), 3 - (i + 1));
}
assert!(typed.is_empty());
assert!(typed.assert_empty().is_ok());
assert_eq!(
typed.next_account().unwrap_err(),
ProgramError::NotEnoughAccountKeys
);
assert_eq!(typed.consumed(), 3);
let mut typed = accounts.typed();
assert_eq!(
typed.take_group(4).err().unwrap(),
ProgramError::NotEnoughAccountKeys
);
let mut group = typed.take_group(3).unwrap();
assert_eq!(group.remaining_len(), 3);
for expected in &views {
assert_eq!(group.next_account().unwrap().address(), expected.address());
}
assert!(group.assert_empty().is_ok());
assert!(typed.is_empty());
let lazy = accounts.lazy();
assert_eq!(lazy.len(), 3);
assert_eq!(lazy.at(2).unwrap().account().address(), views[2].address());
assert_eq!(
lazy.at(3).err().unwrap(),
ProgramError::NotEnoughAccountKeys
);
}
#[test]
fn duplicate_accounts_in_an_instruction_slice_are_rejected() {
let (_a_backing, first) = make_account([7; 32], DEFAULT_OWNER, false, true, 1, b"");
let (_b_backing, second) = make_account([7; 32], DEFAULT_OWNER, false, false, 1, b"");
let accounts = [first, second];
let audit = AccountAudit::new(&accounts);
let duplicate = audit.first_duplicate_writable().unwrap();
assert_eq!(duplicate.first_index, 0);
assert_eq!(duplicate.second_index, 1);
assert_eq!(duplicate.address, Address::new_from_array([7; 32]));
assert_eq!(
audit.require_unique_writable(),
Err(ProgramError::InvalidArgument)
);
assert_eq!(
audit.require_all_unique(),
Err(ProgramError::InvalidArgument)
);
let (_c_backing, first) = make_account([8; 32], DEFAULT_OWNER, true, false, 1, b"");
let (_d_backing, second) = make_account([8; 32], DEFAULT_OWNER, false, false, 1, b"");
let accounts = [first, second];
let audit = AccountAudit::new(&accounts);
assert_eq!(
audit.require_unique_signers(),
Err(ProgramError::InvalidArgument)
);
let (_e_backing, first) = make_account([10; 32], DEFAULT_OWNER, true, true, 1, b"");
let (_f_backing, second) = make_account([11; 32], DEFAULT_OWNER, true, true, 1, b"");
let accounts = [first, second];
let audit = AccountAudit::new(&accounts);
assert_eq!(audit.require_all_unique(), Ok(()));
assert_eq!(audit.require_unique_writable(), Ok(()));
assert_eq!(audit.require_unique_signers(), Ok(()));
assert!(audit.first_duplicate().is_none());
}
#[test]
fn overlapping_mutable_segment_borrows_on_one_account_are_rejected() {
let vault = Address::new_from_array([21; 32]);
let pool = Address::new_from_array([22; 32]);
let mut registry = SegmentBorrowRegistry::new();
registry.register_write(&vault, 0, 16).unwrap();
assert_eq!(
registry.register_write(&vault, 8, 16).unwrap_err(),
ProgramError::AccountBorrowFailed
);
let mut registry = SegmentBorrowRegistry::new();
registry.register_read(&vault, 0, 8).unwrap();
assert_eq!(
registry.register_write(&vault, 4, 8).unwrap_err(),
ProgramError::AccountBorrowFailed
);
let mut registry = SegmentBorrowRegistry::new();
registry.register_write(&vault, 0, 8).unwrap();
assert_eq!(
registry.register_read(&vault, 4, 8).unwrap_err(),
ProgramError::AccountBorrowFailed
);
let mut registry = SegmentBorrowRegistry::new();
registry.register_read(&vault, 0, 8).unwrap();
registry.register_read(&vault, 0, 8).unwrap();
registry.register_write(&vault, 8, 8).unwrap();
registry.register_write(&pool, 8, 8).unwrap();
let mut registry = SegmentBorrowRegistry::new();
let borrow = registry.register_leased_write(&vault, 0, 8).unwrap();
assert_eq!(
registry.register_write(&vault, 0, 8).unwrap_err(),
ProgramError::AccountBorrowFailed
);
assert!(registry.release(&borrow));
registry.register_write(&vault, 0, 8).unwrap();
}
#[repr(C)]
#[derive(Clone, Copy)]
struct CounterV2 {
v: [u8; 8],
}
unsafe impl hopper_runtime::Zeroable for CounterV2 {}
unsafe impl hopper_runtime::Pod for CounterV2 {}
unsafe impl hopper_runtime::__sealed::HopperZeroCopySealed for CounterV2 {}
impl FieldMap for CounterV2 {
const FIELDS: &'static [FieldInfo] = &[FieldInfo::new("v", HopperHeader::SIZE, 8)];
}
impl LayoutContract for CounterV2 {
const DISC: u8 = 77;
const VERSION: u8 = 1;
const LAYOUT_ID: [u8; 8] = [0x77; 8];
const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
const SCHEMA_EPOCH: u32 = 2;
}
fn grow_v1_to_v2(body: &mut [u8]) -> Result<(), ProgramError> {
if body.len() < 8 {
return Err(ProgramError::AccountDataTooSmall);
}
for byte in &mut body[4..8] {
*byte = 0;
}
Ok(())
}
impl LayoutMigration for CounterV2 {
const MIGRATIONS: &'static [MigrationEdge] = &[MigrationEdge {
from_epoch: 1,
to_epoch: 2,
migrator: grow_v1_to_v2,
}];
}
fn make_v1_counter_account(address_byte: u8) -> (Vec<u64>, AccountView<'static>) {
let mut data = [0xAAu8; HopperHeader::SIZE + 8];
data[12..16].copy_from_slice(&1u32.to_le_bytes());
data[HopperHeader::SIZE..HopperHeader::SIZE + 4].copy_from_slice(&[1, 2, 3, 4]);
make_account([address_byte; 32], DEFAULT_OWNER, false, true, 1, &data)
}
#[test]
fn migration_edge_that_grows_the_layout_leaves_no_stale_bytes() {
let (_backing, account) = make_v1_counter_account(30);
let applied =
apply_pending_migrations::<CounterV2>(&account, &Address::new_from_array(DEFAULT_OWNER), 1)
.unwrap();
assert_eq!(applied, 1);
{
let data = account.try_borrow().unwrap();
assert_eq!(&data[12..16], &2u32.to_le_bytes());
assert_eq!(
&data[HopperHeader::SIZE..HopperHeader::SIZE + 4],
&[1, 2, 3, 4]
);
assert_eq!(
&data[HopperHeader::SIZE + 4..HopperHeader::SIZE + 8],
&[0, 0, 0, 0]
);
}
assert_eq!(
apply_pending_migrations::<CounterV2>(&account, &Address::new_from_array(DEFAULT_OWNER), 2),
Ok(0)
);
assert_eq!(
apply_pending_migrations::<CounterV2>(&account, &Address::new_from_array(DEFAULT_OWNER), 3),
Err(ProgramError::InvalidAccountData)
);
}
#[repr(C)]
#[derive(Clone, Copy)]
struct PoisonV2 {
v: [u8; 8],
}
unsafe impl hopper_runtime::Zeroable for PoisonV2 {}
unsafe impl hopper_runtime::Pod for PoisonV2 {}
unsafe impl hopper_runtime::__sealed::HopperZeroCopySealed for PoisonV2 {}
impl FieldMap for PoisonV2 {
const FIELDS: &'static [FieldInfo] = &[FieldInfo::new("v", HopperHeader::SIZE, 8)];
}
impl LayoutContract for PoisonV2 {
const DISC: u8 = 78;
const VERSION: u8 = 1;
const LAYOUT_ID: [u8; 8] = [0x78; 8];
const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
const SCHEMA_EPOCH: u32 = 2;
}
fn failing_migrator(body: &mut [u8]) -> Result<(), ProgramError> {
if let Some(first) = body.first_mut() {
*first = 0xEE;
}
Err(ProgramError::Custom(0xDEAD))
}
impl LayoutMigration for PoisonV2 {
const MIGRATIONS: &'static [MigrationEdge] = &[MigrationEdge {
from_epoch: 1,
to_epoch: 2,
migrator: failing_migrator,
}];
}
#[test]
fn failed_migration_edge_never_advances_the_schema_epoch() {
let mut data = [0u8; HopperHeader::SIZE + 8];
data[12..16].copy_from_slice(&1u32.to_le_bytes());
let (_backing, account) = make_account([31; 32], DEFAULT_OWNER, false, true, 1, &data);
assert_eq!(
apply_pending_migrations::<PoisonV2>(&account, &Address::new_from_array(DEFAULT_OWNER), 1),
Err(ProgramError::Custom(0xDEAD))
);
let data = account.try_borrow().unwrap();
assert_eq!(&data[12..16], &1u32.to_le_bytes());
}
#[test]
fn strict_writes_rejects_writes_outside_the_declared_byte_range_set() {
static POLICY: WritePolicy =
WritePolicy::new(&[WriteRange::new(1, 16, 8), WriteRange::whole_account(2)]);
assert_eq!(POLICY.check_write(0, 0, 8), Err(write_policy_violation(0)));
assert_eq!(POLICY.check_write(0, 0, 1), Err(write_policy_violation(0)));
assert!(POLICY.check_write(1, 16, 8).is_ok());
assert_eq!(POLICY.check_write(1, 0, 8), Err(write_policy_violation(1)));
assert_eq!(POLICY.check_write(1, 24, 8), Err(write_policy_violation(1)));
assert!(POLICY.check_write(1, 20, 8).is_err());
assert!(POLICY.check_write(2, 0, 8).is_ok());
assert!(POLICY.check_write(2, 4096, 1024).is_ok());
static READ_ONLY: WritePolicy = WritePolicy::new(&[]);
assert_eq!(
READ_ONLY.check_write(0, 0, 1),
Err(write_policy_violation(0))
);
assert!(READ_ONLY.check_write(1, 16, 8).is_err());
}
#[test]
fn a_live_segment_borrow_blocks_the_access_a_stale_view_would_need() {
let vault = Address::new_from_array([51; 32]);
let mut registry = SegmentBorrowRegistry::new();
let lease = registry.register_leased_write(&vault, 0, 32).unwrap();
assert_eq!(
registry.register_read(&vault, 16, 8).unwrap_err(),
ProgramError::AccountBorrowFailed
);
registry.register_read(&vault, 32, 8).unwrap();
assert!(registry.release(&lease));
registry.register_write(&vault, 0, 32).unwrap();
let (_backing, account) = make_account([52; 32], DEFAULT_OWNER, false, true, 1, b"payload!");
let live_view = account.try_borrow_mut().unwrap();
assert_eq!(
account.try_borrow().unwrap_err(),
ProgramError::AccountBorrowFailed
);
drop(live_view);
assert!(account.try_borrow().is_ok());
}
#[test]
fn anchor_4616_shared_read_alias_during_live_mutable_borrow_is_refused() {
let slab = Address::new_from_array([61; 32]);
let mut registry = SegmentBorrowRegistry::new();
let lease = registry.register_leased_write(&slab, 8, 32).unwrap();
assert_eq!(
registry.register_read(&slab, 8, 32).unwrap_err(),
ProgramError::AccountBorrowFailed
);
assert_eq!(
registry.register_read(&slab, 32, 16).unwrap_err(),
ProgramError::AccountBorrowFailed
);
registry.register_read(&slab, 40, 8).unwrap();
assert!(registry.release(&lease));
registry.register_read(&slab, 8, 32).unwrap();
let (_backing, account) = make_account([62; 32], DEFAULT_OWNER, false, true, 1, &[0u8; 16]);
let mut registry = SegmentBorrowRegistry::new();
{
let mut header = account.segment_mut::<[u8; 8]>(&mut registry, 0, 8).unwrap();
*header = [0xA5; 8];
assert_eq!(
account.try_borrow().unwrap_err(),
ProgramError::AccountBorrowFailed
);
assert_eq!(
account.try_borrow_mut().unwrap_err(),
ProgramError::AccountBorrowFailed
);
}
let data = account.try_borrow().unwrap();
assert_eq!(&data[..8], &[0xA5; 8]);
}
#[test]
fn anchor_5043_cpi_metas_are_validated_against_the_views_that_back_them() {
use hopper_runtime::{invoke_signed, InstructionAccount, InstructionView};
let callee = Address::new_from_array([0xC0; 32]);
let (_w_backing, writable) = make_account([1; 32], DEFAULT_OWNER, false, true, 1, &[0; 8]);
let (_r_backing, readonly) = make_account([2; 32], DEFAULT_OWNER, false, false, 1, &[0; 8]);
let (_s_backing, signer) = make_account([3; 32], DEFAULT_OWNER, true, false, 1, &[0; 8]);
let data = [1u8, 2, 3];
let metas = [InstructionAccount::writable(readonly.address())];
let ix = InstructionView {
program_id: &callee,
data: &data,
accounts: &metas,
};
assert_eq!(
invoke_signed::<1>(&ix, &[&readonly], &[]),
Err(ProgramError::Immutable)
);
let metas = [InstructionAccount::writable(readonly.address())];
let ix = InstructionView {
program_id: &callee,
data: &data,
accounts: &metas,
};
assert_eq!(
invoke_signed::<1>(&ix, &[&writable], &[]),
Err(ProgramError::InvalidAccountData)
);
let metas = [InstructionAccount::readonly_signer(readonly.address())];
let ix = InstructionView {
program_id: &callee,
data: &data,
accounts: &metas,
};
assert_eq!(
invoke_signed::<1>(&ix, &[&readonly], &[]),
Err(ProgramError::MissingRequiredSignature)
);
let metas = [InstructionAccount::writable(writable.address())];
let ix = InstructionView {
program_id: &callee,
data: &data,
accounts: &metas,
};
{
let _held = writable.try_borrow().unwrap();
assert_eq!(
invoke_signed::<1>(&ix, &[&writable], &[]),
Err(ProgramError::AccountBorrowFailed)
);
}
let metas = [
InstructionAccount::writable(writable.address()),
InstructionAccount::readonly(readonly.address()),
InstructionAccount::readonly_signer(signer.address()),
];
let ix = InstructionView {
program_id: &callee,
data: &data,
accounts: &metas,
};
assert_eq!(
invoke_signed::<3>(&ix, &[&writable, &readonly, &signer], &[]),
Ok(())
);
}