Skip to main content

hopper_core/migrate/
mod.rs

1//! Migration helpers -- safe version upgrades for on-chain accounts.
2//!
3//! Hopper's migration system supports three patterns:
4//!
5//! 1. **Append-safe**: New fields appended to the end, realloc to larger size
6//! 2. **Segment-safe**: New segments added to the segment table
7//! 3. **Full migration**: Data reshuffled between versions
8//!
9//! ## Safety
10//!
11//! - Migration always validates the source layout_id before touching data
12//! - The destination version must be strictly greater than the source
13//! - Realloc is rent-safe (payer provides lamports for the delta)
14
15use crate::account::{read_layout_id, read_version, write_header, FixedLayout};
16use crate::check::{check_owner, check_writable};
17use hopper_runtime::{error::ProgramError, AccountView, Address, ProgramResult};
18
19/// Migrate an account in-place by appending new fields.
20///
21/// This is the cheapest migration: no data movement, just realloc + header update.
22///
23/// ## Preconditions
24///
25/// - Account must be owned by `program_id`
26/// - Account must be writable
27/// - Account layout_id must match `old_layout_id`
28/// - `new_size > old_size` (append-only growth)
29///
30/// ## What it does
31///
32/// 1. Validates ownership, writable, and old layout_id
33/// 2. Reallocs account data to `new_size`
34/// 3. Updates header: new version, new layout_id
35/// 4. Zeroes the newly appended region
36///
37/// New fields are left zero-initialized. The caller should fill them after.
38#[inline]
39#[allow(clippy::too_many_arguments)]
40pub fn migrate_append(
41    account: &AccountView,
42    payer: &AccountView,
43    program_id: &Address,
44    old_layout_id: &[u8; 8],
45    new_version: u8,
46    new_layout_id: &[u8; 8],
47    new_disc: u8,
48    new_size: usize,
49) -> ProgramResult {
50    check_owner(account, program_id)?;
51    check_writable(account)?;
52
53    let data = account.try_borrow()?;
54    let current_layout = read_layout_id(&data)?;
55    if &current_layout != old_layout_id {
56        return Err(ProgramError::InvalidAccountData);
57    }
58    let current_version = read_version(&data)?;
59    if new_version <= current_version {
60        return Err(ProgramError::InvalidAccountData);
61    }
62
63    let old_size = data.len();
64    if new_size <= old_size {
65        return Err(ProgramError::InvalidArgument);
66    }
67
68    // Realloc
69    crate::account::safe_realloc(account, new_size, payer)?;
70
71    // Write updated header
72    let mut data = account.try_borrow_mut()?;
73    write_header(&mut data, new_disc, new_version, new_layout_id)?;
74
75    // Zero the appended region
76    for byte in &mut data[old_size..new_size] {
77        *byte = 0;
78    }
79
80    Ok(())
81}
82
83/// Check if a migration from OldLayout to NewLayout would be append-compatible.
84///
85/// Append-compatible means:
86/// - New layout is strictly larger
87/// - The first `old_size` bytes can stay as-is
88/// - Only new fields were added at the end
89///
90/// This is a compile-time check helper -- use in tests and CI.
91pub const fn is_append_compatible<Old: FixedLayout, New: FixedLayout>() -> bool {
92    New::SIZE > Old::SIZE
93}
94
95/// Migration descriptor for schema export.
96#[derive(Clone, Copy)]
97pub struct MigrationDescriptor {
98    /// Source layout name.
99    pub from_name: &'static str,
100    /// Source version.
101    pub from_version: u8,
102    /// Source layout_id.
103    pub from_layout_id: [u8; 8],
104    /// Target layout name.
105    pub to_name: &'static str,
106    /// Target version.
107    pub to_version: u8,
108    /// Target layout_id.
109    pub to_layout_id: [u8; 8],
110    /// Migration kind.
111    pub kind: MigrationKind,
112}
113
114/// The kind of migration.
115#[derive(Clone, Copy, PartialEq, Eq)]
116pub enum MigrationKind {
117    /// Fields appended to the end -- realloc only, no data movement.
118    Append,
119    /// Segments added to segment table -- realloc + table update.
120    SegmentAppend,
121    /// Full data migration -- copy with transformation.
122    Full,
123}