pub fn migrate_layout<Old, New, F>(
account: &AccountView<'_>,
program_id: &Address,
transform: F,
) -> Result<(), ProgramError>where
Old: LayoutContract + Pod,
New: LayoutContract + Pod,
F: FnOnce(&Old, &mut New) -> 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
Old::validate_header, the full identity check (disc, version, layout_id, epoch). An already-migrated account no longer matchesOldand is refused, which is the idempotence rule: migrate exactly once, route repeat calls to theNewload path.- The
Newshape must FIT the existing allocation (required_len); resizing isrealloc’s job, done separately BEFORE migrating whenNewis larger. Oldis copied to the stack, theNewspan is zeroed (so every field the transform does not set has the framework’s canonical all-zero default, staleOldbytes never leak through), and the typed transform fillsNewfrom the copy.- Only after the transform returns
Okis 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 onOld, 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 asOld.