Skip to main content

migrate_layout

Function migrate_layout 

Source
pub fn migrate_layout<Old, New, F>(
    account: &AccountView<'_>,
    program_id: &Address,
    transform: F,
) -> Result<(), ProgramError>
Expand description

Typed, in-place, cross-VERSION layout migration: Old → New.

The epoch machinery above evolves an account within one layout version through raw &mut [u8] edges. This is the other half of the versioning story: the account’s layout version byte changes (#[hopper::state(version = 1)] → version = 2), the wire fingerprint changes with the field set, and the transform is typed on both sides, no hand-offsetting bytes:

ⓘ
hopper_runtime::migrate::migrate_layout::<VaultV1, VaultV2, _>(
    account,
    program_id,
    |old, new| {
        new.authority = old.authority;
        // Widen the counter; every other V2 field keeps its
        // deterministic all-zero default.
        new.total = WireU64::new(old.total_u32.get() as u64);
        Ok(())
    },
)?;

Contrast with anchor-next’s Migration account shape, which deserializes the old form and RESERIALIZES the new one through borsh. Here both shapes are zero-copy overlays of the same buffer: one stack copy of Old (so the transform can still read it after the buffer is re-purposed), one fill(0) of the New span, no (de)serialization, no heap.

§Sequence

  1. Old::validate_header, the full identity check (disc, version, layout_id, epoch). An already-migrated account no longer matches Old and is refused, which is the idempotence rule: migrate exactly once, route repeat calls to the New load path.
  2. The New shape must FIT the existing allocation (required_len); resizing is realloc’s job, done separately BEFORE migrating when New is larger.
  3. Old is copied to the stack, the New span is zeroed (so every field the transform does not set has the framework’s canonical all-zero default, stale Old bytes never leak through), and the typed transform fills New from the copy.
  4. Only after the transform returns Ok is the header re-stamped, New’s disc/version/layout_id/schema-epoch, with the header’s FLAGS bytes preserved (flags are account state, not layout identity). A transform error therefore leaves the header on Old, same transaction-abort atomicity contract as the epoch edges above: the error must propagate to instruction failure so the runtime rolls the partially-written body back.

§Guard rails (all refuse before touching a byte)

  • New::DISC == Old::DISC, a migration must not repurpose the account kind; both consts are known at monomorphization, so the check folds away when it passes.
  • New::VERSION > Old::VERSION, versions only move forward (also const-folded).
  • The account is writable and owned by program_id, the crank runs at bind BEFORE the per-field validators (so validators see the upgraded account), which means this function is the first authority to look at the account. A user transform must never run over another program’s bytes, however plausibly they parse as Old.