use crate::account::AccountView;
use crate::address::Address;
use crate::error::ProgramError;
use crate::layout::{HopperHeader, LayoutContract};
use crate::zerocopy::AccountLayout;
#[inline(always)]
fn check_migratable(account: &AccountView<'_>, program_id: &Address) -> Result<(), ProgramError> {
if !account.is_writable() {
return Err(ProgramError::InvalidAccountData);
}
if !account.owned_by(program_id) {
return Err(ProgramError::IncorrectProgramId);
}
Ok(())
}
#[derive(Clone, Copy)]
pub struct MigrationEdge {
pub from_epoch: u32,
pub to_epoch: u32,
pub migrator: fn(body: &mut [u8]) -> Result<(), ProgramError>,
}
impl MigrationEdge {
pub const fn is_forward(&self) -> bool {
self.to_epoch > self.from_epoch
}
}
pub trait LayoutMigration {
const MIGRATIONS: &'static [MigrationEdge];
}
#[inline]
pub fn apply_pending_migrations<T>(
account: &AccountView<'_>,
program_id: &Address,
current_epoch: u32,
) -> Result<u32, ProgramError>
where
T: AccountLayout + LayoutContract + LayoutMigration,
{
check_migratable(account, program_id)?;
let target_epoch = <T as AccountLayout>::SCHEMA_EPOCH;
if current_epoch == target_epoch {
return Ok(0);
}
if current_epoch > target_epoch {
return Err(ProgramError::InvalidAccountData);
}
let edges = <T as LayoutMigration>::MIGRATIONS;
let mut applied = 0u32;
let mut epoch = current_epoch;
let mut data = account.try_borrow_mut_ungated()?;
let header_len = core::mem::size_of::<HopperHeader>();
if data.len() < header_len {
return Err(ProgramError::AccountDataTooSmall);
}
while epoch < target_epoch {
let edge = find_edge(edges, epoch)?;
if edge.to_epoch > target_epoch {
return Err(ProgramError::InvalidAccountData);
}
let (header_bytes, body_bytes) = data.split_at_mut(header_len);
(edge.migrator)(body_bytes)?;
let new_epoch_bytes = edge.to_epoch.to_le_bytes();
header_bytes[12..16].copy_from_slice(&new_epoch_bytes);
epoch = edge.to_epoch;
applied += 1;
}
Ok(applied)
}
#[inline]
pub fn migrate_layout<Old, New, F>(
account: &AccountView<'_>,
program_id: &Address,
transform: F,
) -> Result<(), ProgramError>
where
Old: LayoutContract + crate::Pod,
New: LayoutContract + crate::Pod,
F: FnOnce(&Old, &mut New) -> Result<(), ProgramError>,
{
if New::DISC != Old::DISC {
return Err(ProgramError::InvalidAccountData);
}
if New::VERSION <= Old::VERSION {
return Err(ProgramError::InvalidAccountData);
}
check_migratable(account, program_id)?;
let mut data = account.try_borrow_mut_ungated()?;
Old::validate_header(&data)?;
if data.len() < New::required_len() {
return Err(ProgramError::AccountDataTooSmall);
}
let old: Old =
unsafe { core::ptr::read_unaligned((*data).as_ptr().add(Old::TYPE_OFFSET) as *const Old) };
let new_start = New::TYPE_OFFSET;
let new_end = new_start + core::mem::size_of::<New>();
data[new_start..new_end].fill(0);
let new: &mut New = unsafe { &mut *(data.as_bytes_mut_ptr().add(new_start) as *mut New) };
transform(&old, new)?;
let flags = crate::layout::read_flags(&data).unwrap_or(0);
crate::layout::write_header_with_epoch(
&mut data,
New::DISC,
New::VERSION,
&New::LAYOUT_ID,
New::SCHEMA_EPOCH,
)?;
data[2..4].copy_from_slice(&flags.to_le_bytes());
Ok(())
}
#[inline]
pub fn migrate_layout_resizing<Old, New, F>(
account: &AccountView<'_>,
payer: &AccountView<'_>,
program_id: &Address,
shrink_to_fit: bool,
transform: F,
) -> Result<(), ProgramError>
where
Old: LayoutContract + crate::Pod,
New: LayoutContract + crate::Pod,
F: FnOnce(&Old, &mut New) -> Result<(), ProgramError>,
{
if New::DISC != Old::DISC {
return Err(ProgramError::InvalidAccountData);
}
if New::VERSION <= Old::VERSION {
return Err(ProgramError::InvalidAccountData);
}
check_migratable(account, program_id)?;
let new_required = New::required_len();
ensure_fits_with_rent(account, payer, program_id, new_required)?;
migrate_layout::<Old, New, F>(account, program_id, transform)?;
if shrink_to_fit && account.data_len() > new_required {
let min_old = crate::rent::minimum_balance_live(account.data_len())?;
let min_new = crate::rent::minimum_balance_live(new_required)?;
drop(account.try_borrow_mut_ungated()?);
account.resize(new_required)?;
let delta = min_old.saturating_sub(min_new);
let above_min = account.lamports().saturating_sub(min_new);
let refund = if delta < above_min { delta } else { above_min };
if refund > 0 {
if !payer.is_writable() {
return Err(ProgramError::InvalidAccountData);
}
let account_after = account.lamports() - refund;
let payer_after = payer
.lamports()
.checked_add(refund)
.ok_or(ProgramError::ArithmeticOverflow)?;
account.try_set_lamports(account_after)?;
payer.try_set_lamports(payer_after)?;
}
}
Ok(())
}
pub fn ensure_fits_with_rent(
account: &AccountView<'_>,
payer: &AccountView<'_>,
program_id: &Address,
min_len: usize,
) -> Result<(), ProgramError> {
check_migratable(account, program_id)?;
if account.data_len() >= min_len {
return Ok(());
}
let rent_needed = crate::rent::minimum_balance_live(min_len)?;
let deficit = rent_needed.saturating_sub(account.lamports());
if deficit > 0 {
if !payer.is_writable() {
return Err(ProgramError::InvalidAccountData);
}
if !payer.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}
}
drop(account.try_borrow_mut_ungated()?);
account.resize(min_len)?;
if deficit > 0 {
let payer_after = payer
.lamports()
.checked_sub(deficit)
.ok_or(ProgramError::InsufficientFunds)?;
let account_after = account
.lamports()
.checked_add(deficit)
.ok_or(ProgramError::ArithmeticOverflow)?;
payer.try_set_lamports(payer_after)?;
account.try_set_lamports(account_after)?;
}
Ok(())
}
pub fn validate_header_for_epoch_migration<T: LayoutContract>(
data: &[u8],
) -> Result<u32, ProgramError> {
use crate::layout::{
effective_schema_epoch, read_disc, read_layout_id, read_schema_epoch, read_version,
};
if data.len() < T::required_len() {
return Err(ProgramError::AccountDataTooSmall);
}
if read_disc(data) != Some(T::DISC)
|| read_version(data) != Some(T::VERSION)
|| read_layout_id(data) != Some(&T::LAYOUT_ID)
{
return Err(ProgramError::InvalidAccountData);
}
let stored = read_schema_epoch(data).ok_or(ProgramError::InvalidAccountData)?;
let effective = effective_schema_epoch(stored);
if effective > T::SCHEMA_EPOCH {
return Err(ProgramError::InvalidAccountData);
}
Ok(effective)
}
#[macro_export]
macro_rules! migrate_chain {
($account:expr, $program_id:expr, { $($old:ty => $new:ty : $f:expr),+ $(,)? }) => {{
let __hopper_chain_view = $account;
let __hopper_chain_pid = $program_id;
let mut __hopper_chain_hops: u32 = 0;
$(
{
let __hopper_chain_is_old = {
let __hopper_chain_data = __hopper_chain_view.try_borrow()?;
<$old as $crate::LayoutContract>::validate_header(
&__hopper_chain_data,
)
.is_ok()
};
if __hopper_chain_is_old {
$crate::migrate_layout::<$old, $new, _>(
__hopper_chain_view,
__hopper_chain_pid,
$f,
)?;
__hopper_chain_hops += 1;
}
}
)+
__hopper_chain_hops
}};
($account:expr, $program_id:expr, payer = $payer:expr,
{ $($old:ty => $new:ty : $f:expr),+ $(,)? }) => {{
let __hopper_chain_view = $account;
let __hopper_chain_pid = $program_id;
let mut __hopper_chain_max: usize = 0;
$(
{
let __hopper_chain_len =
<$new as $crate::LayoutContract>::required_len();
if __hopper_chain_len > __hopper_chain_max {
__hopper_chain_max = __hopper_chain_len;
}
}
)+
$crate::ensure_fits_with_rent(
__hopper_chain_view,
$payer,
__hopper_chain_pid,
__hopper_chain_max,
)?;
let mut __hopper_chain_hops: u32 = 0;
$(
{
let __hopper_chain_is_old = {
let __hopper_chain_data = __hopper_chain_view.try_borrow()?;
<$old as $crate::LayoutContract>::validate_header(
&__hopper_chain_data,
)
.is_ok()
};
if __hopper_chain_is_old {
$crate::migrate_layout::<$old, $new, _>(
__hopper_chain_view,
__hopper_chain_pid,
$f,
)?;
__hopper_chain_hops += 1;
}
}
)+
__hopper_chain_hops
}};
}
#[inline]
fn find_edge(edges: &[MigrationEdge], epoch: u32) -> Result<&MigrationEdge, ProgramError> {
for edge in edges {
if edge.from_epoch == epoch {
if !edge.is_forward() {
return Err(ProgramError::InvalidAccountData);
}
return Ok(edge);
}
}
Err(ProgramError::InvalidAccountData)
}
#[cfg(test)]
mod tests {
use super::*;
fn identity(_body: &mut [u8]) -> Result<(), ProgramError> {
Ok(())
}
#[test]
fn migration_edge_is_forward_detects_non_monotonic() {
let forward = MigrationEdge {
from_epoch: 1,
to_epoch: 2,
migrator: identity,
};
let backward = MigrationEdge {
from_epoch: 3,
to_epoch: 2,
migrator: identity,
};
let same = MigrationEdge {
from_epoch: 2,
to_epoch: 2,
migrator: identity,
};
assert!(forward.is_forward());
assert!(!backward.is_forward());
assert!(!same.is_forward());
}
#[test]
fn find_edge_returns_matching_edge() {
let edges = [
MigrationEdge {
from_epoch: 1,
to_epoch: 2,
migrator: identity,
},
MigrationEdge {
from_epoch: 2,
to_epoch: 3,
migrator: identity,
},
];
let e1 = find_edge(&edges, 1).expect("edge exists");
assert_eq!(e1.to_epoch, 2);
let e2 = find_edge(&edges, 2).expect("edge exists");
assert_eq!(e2.to_epoch, 3);
}
#[test]
fn find_edge_errs_on_missing_epoch() {
let edges = [MigrationEdge {
from_epoch: 1,
to_epoch: 2,
migrator: identity,
}];
assert!(find_edge(&edges, 5).is_err());
}
#[test]
fn find_edge_rejects_non_forward_edge() {
let edges = [MigrationEdge {
from_epoch: 3,
to_epoch: 2,
migrator: identity,
}];
assert!(find_edge(&edges, 3).is_err());
}
mod overshoot {
use super::*;
use crate::layout::{HopperHeader, LayoutContract};
use crate::zerocopy::AccountLayout;
use hopper_native::{
AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
NOT_BORROWED,
};
#[repr(C)]
#[derive(Clone, Copy)]
struct EpochTwo {
v: [u8; 8],
}
unsafe impl crate::Zeroable for EpochTwo {}
unsafe impl crate::Pod for EpochTwo {}
unsafe impl crate::zerocopy::__sealed::HopperZeroCopySealed for EpochTwo {}
impl crate::field_map::FieldMap for EpochTwo {
const FIELDS: &'static [crate::field_map::FieldInfo] =
&[crate::field_map::FieldInfo::new("v", HopperHeader::SIZE, 8)];
}
impl LayoutContract for EpochTwo {
const DISC: u8 = 91;
const VERSION: u8 = 1;
const LAYOUT_ID: [u8; 8] = [0x91; 8];
const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
const SCHEMA_EPOCH: u32 = 2;
}
impl LayoutMigration for EpochTwo {
const MIGRATIONS: &'static [MigrationEdge] = &[MigrationEdge {
from_epoch: 1,
to_epoch: 3,
migrator: identity,
}];
}
#[test]
fn foreign_owned_account_is_refused_before_any_edge_runs() {
let mut backing =
std::vec![0u64; (RuntimeAccount::SIZE + HopperHeader::SIZE + 8).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: 1,
executable: 0,
resize_delta: 0,
address: NativeAddress::new_from_array([5; 32]),
owner: NativeAddress::new_from_array([9; 32]),
lamports: 1,
data_len: (HopperHeader::SIZE + 8) as u64,
});
}
let backend = unsafe { NativeAccountView::new_unchecked(raw) };
let account = crate::AccountView::from_backend(backend);
assert_eq!(
apply_pending_migrations::<EpochTwo>(
&account,
&Address::new_from_array([6; 32]),
1
),
Err(ProgramError::IncorrectProgramId)
);
}
#[test]
fn overshooting_edge_is_refused_before_writing() {
let mut backing =
std::vec![0u64; (RuntimeAccount::SIZE + HopperHeader::SIZE + 8).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: 1,
executable: 0,
resize_delta: 0,
address: NativeAddress::new_from_array([5; 32]),
owner: NativeAddress::new_from_array([6; 32]),
lamports: 1,
data_len: (HopperHeader::SIZE + 8) as u64,
});
}
let backend = unsafe { NativeAccountView::new_unchecked(raw) };
let account = crate::AccountView::from_backend(backend);
assert_eq!(
apply_pending_migrations::<EpochTwo>(
&account,
&Address::new_from_array([6; 32]),
1
),
Err(ProgramError::InvalidAccountData)
);
let _ = <EpochTwo as AccountLayout>::SCHEMA_EPOCH;
}
}
mod typed_layout_migration {
use super::*;
use crate::layout::{
read_disc, read_flags, read_layout_id, read_schema_epoch, read_version, write_header,
HopperHeader,
};
use hopper_native::{
AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
NOT_BORROWED,
};
const KIND: u8 = 77;
#[repr(C)]
#[derive(Clone, Copy)]
struct VaultV1 {
count: [u8; 4],
legacy: [u8; 4],
}
unsafe impl crate::Zeroable for VaultV1 {}
unsafe impl crate::Pod for VaultV1 {}
unsafe impl crate::zerocopy::__sealed::HopperZeroCopySealed for VaultV1 {}
impl crate::field_map::FieldMap for VaultV1 {
const FIELDS: &'static [crate::field_map::FieldInfo] = &[
crate::field_map::FieldInfo::new("count", HopperHeader::SIZE, 4),
crate::field_map::FieldInfo::new("legacy", HopperHeader::SIZE + 4, 4),
];
}
impl LayoutContract for VaultV1 {
const DISC: u8 = KIND;
const VERSION: u8 = 1;
const LAYOUT_ID: [u8; 8] = [0x11; 8];
const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
}
#[repr(C)]
#[derive(Clone, Copy)]
struct VaultV2 {
count: [u8; 8],
flag: u8,
pad: [u8; 3],
}
unsafe impl crate::Zeroable for VaultV2 {}
unsafe impl crate::Pod for VaultV2 {}
unsafe impl crate::zerocopy::__sealed::HopperZeroCopySealed for VaultV2 {}
impl crate::field_map::FieldMap for VaultV2 {
const FIELDS: &'static [crate::field_map::FieldInfo] = &[
crate::field_map::FieldInfo::new("count", HopperHeader::SIZE, 8),
crate::field_map::FieldInfo::new("flag", HopperHeader::SIZE + 8, 1),
crate::field_map::FieldInfo::new("pad", HopperHeader::SIZE + 9, 3),
];
}
impl LayoutContract for VaultV2 {
const DISC: u8 = KIND;
const VERSION: u8 = 2;
const LAYOUT_ID: [u8; 8] = [0x22; 8];
const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
const SCHEMA_EPOCH: u32 = 5;
}
#[repr(C)]
#[derive(Clone, Copy)]
struct OtherKind {
v: [u8; 8],
}
unsafe impl crate::Zeroable for OtherKind {}
unsafe impl crate::Pod for OtherKind {}
unsafe impl crate::zerocopy::__sealed::HopperZeroCopySealed for OtherKind {}
impl crate::field_map::FieldMap for OtherKind {
const FIELDS: &'static [crate::field_map::FieldInfo] =
&[crate::field_map::FieldInfo::new("v", HopperHeader::SIZE, 8)];
}
impl LayoutContract for OtherKind {
const DISC: u8 = KIND + 1;
const VERSION: u8 = 3;
const LAYOUT_ID: [u8; 8] = [0x33; 8];
const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
}
#[repr(C)]
#[derive(Clone, Copy)]
struct VaultV1b {
count: [u8; 8],
}
unsafe impl crate::Zeroable for VaultV1b {}
unsafe impl crate::Pod for VaultV1b {}
unsafe impl crate::zerocopy::__sealed::HopperZeroCopySealed for VaultV1b {}
impl crate::field_map::FieldMap for VaultV1b {
const FIELDS: &'static [crate::field_map::FieldInfo] =
&[crate::field_map::FieldInfo::new(
"count",
HopperHeader::SIZE,
8,
)];
}
impl LayoutContract for VaultV1b {
const DISC: u8 = KIND;
const VERSION: u8 = 1;
const LAYOUT_ID: [u8; 8] = [0x44; 8];
const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
}
fn pid() -> crate::address::Address {
crate::address::Address::new_from_array([6; 32])
}
fn raw_account(
data_len: usize,
lamports: u64,
is_writable: bool,
is_signer: bool,
owner: [u8; 32],
) -> (std::vec::Vec<u64>, crate::AccountView<'static>) {
use hopper_native::MAX_PERMITTED_DATA_INCREASE;
let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + data_len + MAX_PERMITTED_DATA_INCREASE).div_ceil(8)];
let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
unsafe {
raw.write(RuntimeAccount {
borrow_state: NOT_BORROWED,
is_signer: is_signer as u8,
is_writable: is_writable as u8,
executable: 0,
resize_delta: 0,
address: NativeAddress::new_from_array([5; 32]),
owner: NativeAddress::new_from_array(owner),
lamports,
data_len: data_len as u64,
});
}
let backend = unsafe { NativeAccountView::new_unchecked(raw) };
(backing, crate::AccountView::from_backend(backend))
}
fn stamp_v1(account: &crate::AccountView<'_>) {
let mut data = account.try_borrow_mut().expect("fixture borrow");
write_header(
&mut data,
<VaultV1 as LayoutContract>::DISC,
<VaultV1 as LayoutContract>::VERSION,
&<VaultV1 as LayoutContract>::LAYOUT_ID,
)
.expect("fixture header");
data[2..4].copy_from_slice(&0x0102u16.to_le_bytes());
data[16..20].copy_from_slice(&7u32.to_le_bytes());
data[20..24].copy_from_slice(&[1, 2, 3, 4]);
}
fn seeded_v1(data_len: usize) -> (std::vec::Vec<u64>, crate::AccountView<'static>) {
let (backing, account) = raw_account(data_len, 1, true, false, [6; 32]);
stamp_v1(&account);
(backing, account)
}
fn widen(old: &VaultV1, new: &mut VaultV2) -> Result<(), ProgramError> {
let count = u32::from_le_bytes(old.count) as u64;
new.count = count.to_le_bytes();
new.flag = 1;
Ok(())
}
#[test]
fn typed_migration_restamps_header_and_transforms_body() {
let (_b, account) = seeded_v1(HopperHeader::SIZE + 16);
migrate_layout::<VaultV1, VaultV2, _>(&account, &pid(), widen).expect("migrates");
let data = account.try_borrow().expect("read back");
assert_eq!(read_disc(&data), Some(KIND));
assert_eq!(read_version(&data), Some(2));
assert_eq!(read_layout_id(&data), Some(&[0x22; 8]));
assert_eq!(read_schema_epoch(&data), Some(5));
assert_eq!(
read_flags(&data),
Some(0x0102),
"flags are account state and must survive the re-stamp"
);
assert_eq!(&data[16..24], &7u64.to_le_bytes());
assert_eq!(data[24], 1);
assert_eq!(&data[25..28], &[0, 0, 0]);
}
#[test]
fn migrated_account_refuses_a_second_migration() {
let (_b, account) = seeded_v1(HopperHeader::SIZE + 16);
migrate_layout::<VaultV1, VaultV2, _>(&account, &pid(), widen).expect("first migrates");
assert_eq!(
migrate_layout::<VaultV1, VaultV2, _>(&account, &pid(), widen),
Err(ProgramError::InvalidAccountData)
);
}
#[test]
fn transform_error_leaves_the_header_on_the_old_layout() {
let (_b, account) = seeded_v1(HopperHeader::SIZE + 16);
let result = migrate_layout::<VaultV1, VaultV2, _>(&account, &pid(), |_, _| {
Err(ProgramError::Custom(9))
});
assert_eq!(result, Err(ProgramError::Custom(9)));
let data = account.try_borrow().expect("read back");
assert_eq!(read_version(&data), Some(1));
assert_eq!(read_layout_id(&data), Some(&[0x11; 8]));
}
#[test]
fn larger_new_shape_requires_realloc_first() {
let (_b, account) = seeded_v1(HopperHeader::SIZE + 8);
assert_eq!(
migrate_layout::<VaultV1, VaultV2, _>(&account, &pid(), widen),
Err(ProgramError::AccountDataTooSmall)
);
let data = account.try_borrow().expect("read back");
assert_eq!(read_version(&data), Some(1), "refused before any write");
assert_eq!(&data[16..20], &7u32.to_le_bytes());
}
#[test]
fn cross_kind_migration_is_refused() {
let (_b, account) = seeded_v1(HopperHeader::SIZE + 16);
assert_eq!(
migrate_layout::<VaultV1, OtherKind, _>(&account, &pid(), |_, _| Ok(())),
Err(ProgramError::InvalidAccountData)
);
}
#[test]
fn non_forward_version_is_refused() {
let (_b, account) = seeded_v1(HopperHeader::SIZE + 16);
assert_eq!(
migrate_layout::<VaultV1, VaultV1b, _>(&account, &pid(), |_, _| Ok(())),
Err(ProgramError::InvalidAccountData)
);
assert_eq!(
migrate_layout::<VaultV2, VaultV1b, _>(&account, &pid(), |_, _| Ok(())),
Err(ProgramError::InvalidAccountData)
);
}
#[test]
fn foreign_owned_account_is_refused_before_the_transform_runs() {
let (_b, account) = raw_account(HopperHeader::SIZE + 16, 1, true, false, [9; 32]);
stamp_v1(&account);
let mut transform_ran = false;
let result = migrate_layout::<VaultV1, VaultV2, _>(&account, &pid(), |old, new| {
transform_ran = true;
widen(old, new)
});
assert_eq!(result, Err(ProgramError::IncorrectProgramId));
assert!(
!transform_ran,
"the user transform must never run over another program's bytes"
);
let data = account.try_borrow().expect("read back");
assert_eq!(read_version(&data), Some(1), "nothing was written");
}
#[test]
fn non_writable_account_is_refused_before_the_transform_runs() {
let (_b, account) = raw_account(HopperHeader::SIZE + 16, 1, false, false, [6; 32]);
stamp_v1(&account);
assert_eq!(
migrate_layout::<VaultV1, VaultV2, _>(&account, &pid(), widen),
Err(ProgramError::InvalidAccountData)
);
}
#[test]
fn resizing_migration_grows_and_tops_up_exactly_the_deficit() {
use crate::rent::minimum_balance_live;
let (_b, account) = raw_account(HopperHeader::SIZE + 8, 1, true, false, [6; 32]);
stamp_v1(&account);
let payer_start = 1_000_000_000u64;
let (_pb, payer) = raw_account(0, payer_start, true, true, [0; 32]);
migrate_layout_resizing::<VaultV1, VaultV2, _>(&account, &payer, &pid(), false, widen)
.expect("grow + migrate");
let min_new = minimum_balance_live(HopperHeader::SIZE + 12).expect("host rent");
assert_eq!(account.data_len(), HopperHeader::SIZE + 12);
assert_eq!(account.lamports(), min_new, "topped up to the minimum");
assert_eq!(
payer.lamports(),
payer_start - (min_new - 1),
"payer debited exactly the deficit"
);
let data = account.try_borrow().expect("read back");
assert_eq!(read_version(&data), Some(2));
assert_eq!(&data[16..24], &7u64.to_le_bytes());
}
#[test]
fn resizing_migration_needs_no_payer_when_already_funded() {
use crate::rent::minimum_balance_live;
let funded = minimum_balance_live(HopperHeader::SIZE + 12).expect("host rent") + 777;
let (_b, account) = raw_account(HopperHeader::SIZE + 8, funded, true, false, [6; 32]);
stamp_v1(&account);
let (_pb, payer) = raw_account(0, 5, false, false, [0; 32]);
migrate_layout_resizing::<VaultV1, VaultV2, _>(&account, &payer, &pid(), false, widen)
.expect("grow without payer");
assert_eq!(account.lamports(), funded, "balance untouched");
assert_eq!(payer.lamports(), 5, "payer untouched");
}
#[repr(C)]
#[derive(Clone, Copy)]
struct VaultV3 {
count: [u8; 4],
}
unsafe impl crate::Zeroable for VaultV3 {}
unsafe impl crate::Pod for VaultV3 {}
unsafe impl crate::zerocopy::__sealed::HopperZeroCopySealed for VaultV3 {}
impl crate::field_map::FieldMap for VaultV3 {
const FIELDS: &'static [crate::field_map::FieldInfo] =
&[crate::field_map::FieldInfo::new(
"count",
HopperHeader::SIZE,
4,
)];
}
impl LayoutContract for VaultV3 {
const DISC: u8 = KIND;
const VERSION: u8 = 3;
const LAYOUT_ID: [u8; 8] = [0x55; 8];
const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
}
fn narrow(old: &VaultV2, new: &mut VaultV3) -> Result<(), ProgramError> {
let count = u64::from_le_bytes(old.count) as u32;
new.count = count.to_le_bytes();
Ok(())
}
fn seeded_v2(lamports: u64) -> (std::vec::Vec<u64>, crate::AccountView<'static>) {
let (backing, account) = raw_account(HopperHeader::SIZE + 12, 1, true, false, [6; 32]);
stamp_v1(&account);
migrate_layout::<VaultV1, VaultV2, _>(&account, &pid(), widen).expect("to V2");
account.try_set_lamports(lamports).expect("fund fixture");
(backing, account)
}
#[test]
fn shrink_refunds_only_the_rent_delta_and_never_touches_deposits() {
use crate::rent::minimum_balance_live;
let min_old = minimum_balance_live(HopperHeader::SIZE + 12).expect("host rent");
let min_new = minimum_balance_live(HopperHeader::SIZE + 4).expect("host rent");
let deposit = 500_000u64;
let (_b, account) = seeded_v2(min_old + deposit);
let (_pb, payer) = raw_account(0, 10, true, false, [0; 32]);
migrate_layout_resizing::<VaultV2, VaultV3, _>(&account, &payer, &pid(), true, narrow)
.expect("migrate + shrink");
assert_eq!(account.data_len(), HopperHeader::SIZE + 4);
assert_eq!(
account.lamports(),
min_new + deposit,
"the deposit MUST stay on the account, only the freed \
rent requirement is refunded"
);
assert_eq!(
payer.lamports(),
10 + (min_old - min_new),
"payer receives exactly the rent delta"
);
}
#[test]
fn migrate_chain_heals_from_any_starting_version() {
fn run_chain(account: &crate::AccountView<'_>) -> Result<u32, ProgramError> {
Ok(crate::migrate_chain!(account, &pid(), {
VaultV1 => VaultV2: widen,
VaultV2 => VaultV3: narrow,
}))
}
let (_b, v1) = seeded_v1(HopperHeader::SIZE + 16);
assert_eq!(run_chain(&v1), Ok(2));
{
let data = v1.try_borrow().unwrap();
assert_eq!(read_version(&data), Some(3));
assert_eq!(&data[16..20], &7u32.to_le_bytes());
}
let (_b2, v2) = seeded_v2(1);
assert_eq!(run_chain(&v2), Ok(1));
assert_eq!(read_version(&v2.try_borrow().unwrap()), Some(3));
assert_eq!(run_chain(&v2), Ok(0));
let (_b3, other) = raw_account(HopperHeader::SIZE + 16, 1, true, false, [6; 32]);
{
let mut data = other.try_borrow_mut().unwrap();
write_header(
&mut data,
<OtherKind as LayoutContract>::DISC,
<OtherKind as LayoutContract>::VERSION,
&<OtherKind as LayoutContract>::LAYOUT_ID,
)
.unwrap();
}
assert_eq!(run_chain(&other), Ok(0));
assert_eq!(
read_disc(&other.try_borrow().unwrap()),
Some(<OtherKind as LayoutContract>::DISC)
);
}
#[test]
fn migrate_chain_with_payer_grows_once_to_the_largest_hop() {
fn run_chain<'a>(
account: &crate::AccountView<'a>,
payer: &crate::AccountView<'a>,
) -> Result<u32, ProgramError> {
Ok(crate::migrate_chain!(account, &pid(), payer = payer, {
VaultV1 => VaultV2: widen,
VaultV2 => VaultV3: narrow,
}))
}
let (_b, account) = raw_account(HopperHeader::SIZE + 8, 1, true, false, [6; 32]);
stamp_v1(&account);
let payer_start = 1_000_000_000u64;
let (_pb, payer) = raw_account(0, payer_start, true, true, [0; 32]);
assert_eq!(run_chain(&account, &payer), Ok(2));
assert_eq!(account.data_len(), HopperHeader::SIZE + 12);
let data = account.try_borrow().unwrap();
assert_eq!(read_version(&data), Some(3));
assert_eq!(&data[16..20], &7u32.to_le_bytes());
assert!(payer.lamports() < payer_start, "payer funded the grow");
}
#[test]
fn epoch_migration_header_predicate_accepts_lag_refuses_lead() {
use crate::layout::write_header_with_epoch;
#[repr(C)]
#[derive(Clone, Copy)]
struct EpochThree {
v: [u8; 8],
}
unsafe impl crate::Zeroable for EpochThree {}
unsafe impl crate::Pod for EpochThree {}
unsafe impl crate::zerocopy::__sealed::HopperZeroCopySealed for EpochThree {}
impl crate::field_map::FieldMap for EpochThree {
const FIELDS: &'static [crate::field_map::FieldInfo] =
&[crate::field_map::FieldInfo::new("v", HopperHeader::SIZE, 8)];
}
impl LayoutContract for EpochThree {
const DISC: u8 = 93;
const VERSION: u8 = 1;
const LAYOUT_ID: [u8; 8] = [0x93; 8];
const SIZE: usize = HopperHeader::SIZE + 8;
const SCHEMA_EPOCH: u32 = 3;
}
let mut data = std::vec![0u8; HopperHeader::SIZE + 8];
write_header_with_epoch(&mut data, 93, 1, &[0x93; 8], 1).unwrap();
assert_eq!(
validate_header_for_epoch_migration::<EpochThree>(&data),
Ok(1)
);
write_header_with_epoch(&mut data, 93, 1, &[0x93; 8], 3).unwrap();
assert_eq!(
validate_header_for_epoch_migration::<EpochThree>(&data),
Ok(3)
);
write_header_with_epoch(&mut data, 93, 1, &[0x93; 8], 4).unwrap();
assert!(validate_header_for_epoch_migration::<EpochThree>(&data).is_err());
write_header_with_epoch(&mut data, 93, 1, &[0x93; 8], 0).unwrap();
assert_eq!(
validate_header_for_epoch_migration::<EpochThree>(&data),
Ok(1)
);
write_header_with_epoch(&mut data, 94, 1, &[0x93; 8], 1).unwrap();
assert!(validate_header_for_epoch_migration::<EpochThree>(&data).is_err());
write_header_with_epoch(&mut data, 93, 2, &[0x93; 8], 1).unwrap();
assert!(validate_header_for_epoch_migration::<EpochThree>(&data).is_err());
write_header_with_epoch(&mut data, 93, 1, &[0x44; 8], 1).unwrap();
assert!(validate_header_for_epoch_migration::<EpochThree>(&data).is_err());
let tiny = std::vec![0u8; HopperHeader::SIZE + 4];
assert!(validate_header_for_epoch_migration::<EpochThree>(&tiny).is_err());
}
#[test]
fn shrink_is_opt_in_and_off_by_default_in_the_macro() {
use crate::rent::minimum_balance_live;
let min_old = minimum_balance_live(HopperHeader::SIZE + 12).expect("host rent");
let (_b, account) = seeded_v2(min_old);
let (_pb, payer) = raw_account(0, 10, true, false, [0; 32]);
migrate_layout_resizing::<VaultV2, VaultV3, _>(&account, &payer, &pid(), false, narrow)
.expect("migrate without shrink");
assert_eq!(account.data_len(), HopperHeader::SIZE + 12, "size kept");
assert_eq!(account.lamports(), min_old, "no refund");
assert_eq!(payer.lamports(), 10);
}
}
}