diff --git a/CHANGELOG.md b/CHANGELOG.md index afe55c7fa9..ed453b4268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The minor version will be incremented upon a breaking change and the patch versi ### Features +- lang: Add `Migration<'info, From, To>` account type for schema migrations between account types ([#4060](https://github.com/solana-foundation/anchor/pull/4060)). - cli: Added a `check_program_id_mismatch` in build time to check if the program ID in the source code matches the program ID in the keypair file ([#4018](https://github.com/solana-foundation/anchor/pull/4018)). This check will be skipped during `anchor test`. ### Fixes diff --git a/docs/content/docs/references/account-types.mdx b/docs/content/docs/references/account-types.mdx index 8317df882e..9663cea694 100644 --- a/docs/content/docs/references/account-types.mdx +++ b/docs/content/docs/references/account-types.mdx @@ -230,3 +230,74 @@ pub struct InstructionAccounts<'info> { pub account: UncheckedAccount<'info>, } ``` + +### `Migration<'info, From, To>` + +Description: Account container that handles schema migrations from one account type (`From`) to another (`To`). During deserialization, the account must be in the `From` format. On instruction exit, the account must be migrated to the `To` format, which is then serialized. Typically used with the `realloc` constraint to resize accounts during migration. + +Checks: +- `Account.info.owner == From::owner()` +- Account is initialized (not owned by system program with 0 lamports) +- Account deserializes as `From` type + +```rust title="snippet" +use anchor_lang::prelude::*; + +#[account] +pub struct AccountV1 { + pub data: u64, +} + +#[account] +pub struct AccountV2 { + pub data: u64, + pub new_field: u64, +} + +#[derive(Accounts)] +pub struct MigrateAccount<'info> { + #[account(mut)] + pub payer: Signer<'info>, + // [!code word:Migration] + // [!code highlight:5] + #[account( + mut, + realloc = 8 + AccountV2::INIT_SPACE, + realloc::payer = payer, + realloc::zero = false + )] + pub my_account: Migration<'info, AccountV1, AccountV2>, + pub system_program: Program<'info, System>, +} +``` + +Usage patterns: + +```rust title="migrate with explicit call" +// Access old fields via Deref before migration +let old_data = ctx.accounts.my_account.data; + +// Migrate to new schema +ctx.accounts.my_account.migrate(AccountV2 { + data: old_data, + new_field: 42, +})?; +``` + +```rust title="idempotent migration with into_inner" +// Migrates if needed, returns reference to new data +let migrated = ctx.accounts.my_account.into_inner(AccountV2 { + data: ctx.accounts.my_account.data, + new_field: ctx.accounts.my_account.data * 2, +}); +msg!("New field: {}", migrated.new_field); +``` + +```rust title="idempotent migration with mutation" +// Migrates if needed, returns mutable reference +let migrated = ctx.accounts.my_account.into_inner_mut(AccountV2 { + data: ctx.accounts.my_account.data, + new_field: 0, +}); +migrated.new_field = 42; +``` diff --git a/lang/src/accounts/migration.rs b/lang/src/accounts/migration.rs new file mode 100644 index 0000000000..fdf43abe22 --- /dev/null +++ b/lang/src/accounts/migration.rs @@ -0,0 +1,857 @@ +//! Account container for migrating from one account type to another. + +use crate::bpf_writer::BpfWriter; +use crate::error::{Error, ErrorCode}; +use crate::solana_program::account_info::AccountInfo; +use crate::solana_program::instruction::AccountMeta; +use crate::solana_program::pubkey::Pubkey; +use crate::solana_program::system_program; +use crate::{ + AccountDeserialize, AccountSerialize, Accounts, AccountsExit, Key, Owner, Result, + ToAccountInfos, ToAccountMetas, +}; +use std::collections::BTreeSet; +use std::ops::{Deref, DerefMut}; + +/// Internal representation of the migration state. +#[derive(Debug)] +pub enum MigrationInner { + /// Account is in old format, will be migrated and serialized on exit + From(From), + /// Account is already in new format, will be serialized on exit + To(To), +} + +/// Wrapper around [`AccountInfo`](crate::solana_program::account_info::AccountInfo) +/// that handles account schema migrations from one type to another. +/// +/// # Table of Contents +/// - [Basic Functionality](#basic-functionality) +/// - [Usage Patterns](#usage-patterns) +/// - [Example](#example) +/// +/// # Basic Functionality +/// +/// `Migration` facilitates migrating account data from an old schema (`From`) to a new +/// schema (`To`). During deserialization, the account must be in the `From` format - +/// accounts already in the `To` format will be rejected with an error. +/// +/// The migrated data is stored in memory and will be serialized to the account when the +/// instruction exits. On exit, the account must be in the migrated state or an error will +/// be returned. +/// +/// This type is typically used with the `realloc` constraint to resize the account +/// during migration. +/// +/// Checks: +/// +/// - `Account.info.owner == From::owner()` +/// - `!(Account.info.owner == SystemProgram && Account.info.lamports() == 0)` +/// - Account must deserialize as `From` (not `To`) +/// +/// # Usage Patterns +/// +/// There are multiple ways to work with Migration accounts: +/// +/// ## 1. Explicit Migration with `migrate()` +/// +/// ```ignore +/// ctx.accounts.my_account.migrate(AccountV2 { +/// data: ctx.accounts.my_account.data, +/// new_field: 42, +/// })?; +/// ``` +/// +/// ## 2. Direct Field Access via Deref (before migration) +/// +/// ```ignore +/// // Access old account fields directly +/// let old_value = ctx.accounts.my_account.data; +/// let old_timestamp = ctx.accounts.my_account.timestamp; +/// +/// // Then migrate +/// ctx.accounts.my_account.migrate(AccountV2 { ... })?; +/// ``` +/// +/// ## 3. Idempotent Migration with `into_inner()` +/// +/// ```ignore +/// // Migrates if needed, returns reference to new data +/// // Access old fields directly via deref! +/// let migrated = ctx.accounts.my_account.into_inner(AccountV2 { +/// data: ctx.accounts.my_account.data, +/// new_field: ctx.accounts.my_account.data * 2, +/// })?; +/// +/// // Use migrated data (safe to call multiple times!) +/// msg!("New field: {}", migrated.new_field); +/// ``` +/// +/// ## 4. Idempotent Migration with Mutation via `into_inner_mut()` +/// +/// ```ignore +/// // Migrates if needed, returns mutable reference +/// let migrated = ctx.accounts.my_account.into_inner_mut(AccountV2 { +/// data: ctx.accounts.my_account.data, +/// new_field: 0, +/// })?; +/// +/// // Mutate the new data +/// migrated.new_field = 42; +/// ``` +/// +/// # Example +/// ```ignore +/// use anchor_lang::prelude::*; +/// +/// declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); +/// +/// #[program] +/// pub mod my_program { +/// use super::*; +/// +/// pub fn migrate(ctx: Context) -> Result<()> { +/// // Use idempotent migration with into_inner +/// let migrated = ctx.accounts.my_account.into_inner(AccountV2 { +/// data: ctx.accounts.my_account.data, +/// new_field: ctx.accounts.my_account.data * 2, +/// })?; +/// +/// msg!("Migrated! New field: {}", migrated.new_field); +/// Ok(()) +/// } +/// } +/// +/// #[account] +/// pub struct AccountV1 { +/// pub data: u64, +/// } +/// +/// #[account] +/// pub struct AccountV2 { +/// pub data: u64, +/// pub new_field: u64, +/// } +/// +/// #[derive(Accounts)] +/// pub struct MigrateAccount<'info> { +/// #[account(mut)] +/// pub payer: Signer<'info>, +/// #[account( +/// mut, +/// realloc = 8 + AccountV2::INIT_SPACE, +/// realloc::payer = payer, +/// realloc::zero = false +/// )] +/// pub my_account: Migration<'info, AccountV1, AccountV2>, +/// pub system_program: Program<'info, System>, +/// } +/// ``` +#[derive(Debug)] +pub struct Migration<'info, From, To> +where + From: AccountDeserialize, + To: AccountSerialize, +{ + /// Account info reference + info: &'info AccountInfo<'info>, + /// Internal migration state + inner: MigrationInner, +} + +impl<'info, From, To> Migration<'info, From, To> +where + From: AccountDeserialize + Owner, + To: AccountSerialize + Owner, +{ + /// Creates a new Migration in the From (unmigrated) state. + fn new(info: &'info AccountInfo<'info>, account: From) -> Self { + Self { + info, + inner: MigrationInner::From(account), + } + } + + /// Returns `true` if the account has been migrated. + #[inline(always)] + pub fn is_migrated(&self) -> bool { + matches!(self.inner, MigrationInner::To(_)) + } + + /// Returns a reference to the old account data if not yet migrated. + /// + /// # Errors + /// Returns an error if the account has already been migrated. + pub fn try_as_from(&self) -> Result<&From> { + match &self.inner { + MigrationInner::From(from) => Ok(from), + MigrationInner::To(_) => Err(ErrorCode::AccountAlreadyMigrated.into()), + } + } + + /// Returns a mutable reference to the old account data if not yet migrated. + /// + /// # Errors + /// Returns an error if the account has already been migrated. + pub fn try_as_from_mut(&mut self) -> Result<&mut From> { + match &mut self.inner { + MigrationInner::From(from) => Ok(from), + MigrationInner::To(_) => Err(ErrorCode::AccountAlreadyMigrated.into()), + } + } + + /// Migrates the account by providing the new data. + /// + /// This method stores the new data in memory. The data will be + /// serialized to the account when the instruction exits. + /// + /// # Errors + /// Returns an error if the account has already been migrated. + pub fn migrate(&mut self, new_data: To) -> Result<()> { + if self.is_migrated() { + return Err(ErrorCode::AccountAlreadyMigrated.into()); + } + + self.inner = MigrationInner::To(new_data); + Ok(()) + } + + /// Gets a reference to the migrated value, or migrates it with the provided data. + /// + /// This method provides flexible access to the migrated state: + /// - If already migrated, returns a reference to the existing value + /// - If not migrated, migrates with the provided data, then returns a reference + /// + /// # Arguments + /// * `new_data` - The new `To` value to migrate to (only used if not yet migrated) + /// + /// # Example + /// ```ignore + /// pub fn process(ctx: Context) -> Result<()> { + /// // Migrate and get reference in one call + /// // Access old fields directly via deref! + /// let migrated = ctx.accounts.my_account.into_inner(AccountV2 { + /// data: ctx.accounts.my_account.data, + /// new_field: 42, + /// })?; + /// + /// // Use migrated... + /// msg!("Migrated data: {}", migrated.data); + /// + /// Ok(()) + /// } + /// ``` + pub fn into_inner(&mut self, new_data: To) -> &To { + if !self.is_migrated() { + self.inner = MigrationInner::To(new_data); + } + + match &self.inner { + MigrationInner::To(to) => to, + _ => unreachable!(), + } + } + + /// Gets a mutable reference to the migrated value, or migrates it with the provided data. + /// + /// This method provides flexible mutable access to the migrated state: + /// - If already migrated, returns a mutable reference to the existing value + /// - If not migrated, migrates with the provided data, then returns a mutable reference + /// + /// # Arguments + /// * `new_data` - The new `To` value to migrate to (only used if not yet migrated) + /// + /// # Example + /// ```ignore + /// pub fn process(ctx: Context) -> Result<()> { + /// // Migrate and get mutable reference in one call + /// // Access old fields directly via deref! + /// let migrated = ctx.accounts.my_account.into_inner_mut(AccountV2 { + /// data: ctx.accounts.my_account.data, + /// new_field: 0, + /// })?; + /// + /// // Mutate the migrated value + /// migrated.new_field = 42; + /// + /// Ok(()) + /// } + /// ``` + pub fn into_inner_mut(&mut self, new_data: To) -> &mut To { + if !self.is_migrated() { + self.inner = MigrationInner::To(new_data); + } + + match &mut self.inner { + MigrationInner::To(to) => to, + _ => unreachable!(), + } + } + + /// Deserializes the given `info` into a `Migration`. + /// + /// Only accepts accounts in the `From` format. Accounts already in the `To` + /// format will be rejected. + #[inline(never)] + pub fn try_from(info: &'info AccountInfo<'info>) -> Result { + if info.owner == &system_program::ID && info.lamports() == 0 { + return Err(ErrorCode::AccountNotInitialized.into()); + } + + if info.owner != &From::owner() { + return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram) + .with_pubkeys((*info.owner, From::owner()))); + } + + let mut data: &[u8] = &info.try_borrow_data()?; + Ok(Self::new(info, From::try_deserialize(&mut data)?)) + } + + /// Deserializes the given `info` into a `Migration` without checking + /// the account discriminator. + /// + /// **Warning:** Use with caution. This skips discriminator validation. + #[inline(never)] + pub fn try_from_unchecked(info: &'info AccountInfo<'info>) -> Result { + if info.owner == &system_program::ID && info.lamports() == 0 { + return Err(ErrorCode::AccountNotInitialized.into()); + } + + if info.owner != &From::owner() { + return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram) + .with_pubkeys((*info.owner, From::owner()))); + } + + let mut data: &[u8] = &info.try_borrow_data()?; + Ok(Self::new(info, From::try_deserialize_unchecked(&mut data)?)) + } +} + +impl<'info, B, From, To> Accounts<'info, B> for Migration<'info, From, To> +where + From: AccountDeserialize + Owner, + To: AccountSerialize + Owner, +{ + #[inline(never)] + fn try_accounts( + _program_id: &Pubkey, + accounts: &mut &'info [AccountInfo<'info>], + _ix_data: &[u8], + _bumps: &mut B, + _reallocs: &mut BTreeSet, + ) -> Result { + if accounts.is_empty() { + return Err(ErrorCode::AccountNotEnoughKeys.into()); + } + let account = &accounts[0]; + *accounts = &accounts[1..]; + Self::try_from(account) + } +} + +impl<'info, From, To> AccountsExit<'info> for Migration<'info, From, To> +where + From: AccountDeserialize + Owner, + To: AccountSerialize + Owner, +{ + fn exit(&self, program_id: &Pubkey) -> Result<()> { + // Check if account is closed + if crate::common::is_closed(self.info) { + return Ok(()); + } + + // Check that the account has been migrated and serialize + match &self.inner { + MigrationInner::From(_) => { + // Account was not migrated - this is an error + return Err(ErrorCode::AccountNotMigrated.into()); + } + MigrationInner::To(to) => { + // Only persist if the owner is the current program + let expected_owner = To::owner(); + if &expected_owner != program_id { + return Ok(()); + } + + // Serialize the migrated data + let mut data = self.info.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + let mut writer = BpfWriter::new(dst); + to.try_serialize(&mut writer)?; + } + } + + Ok(()) + } +} + +impl ToAccountMetas for Migration<'_, From, To> +where + From: AccountDeserialize, + To: AccountSerialize, +{ + fn to_account_metas(&self, is_signer: Option) -> Vec { + let is_signer = is_signer.unwrap_or(self.info.is_signer); + let meta = match self.info.is_writable { + false => AccountMeta::new_readonly(*self.info.key, is_signer), + true => AccountMeta::new(*self.info.key, is_signer), + }; + vec![meta] + } +} + +impl<'info, From, To> ToAccountInfos<'info> for Migration<'info, From, To> +where + From: AccountDeserialize, + To: AccountSerialize, +{ + fn to_account_infos(&self) -> Vec> { + vec![self.info.clone()] + } +} + +impl<'info, From, To> AsRef> for Migration<'info, From, To> +where + From: AccountDeserialize, + To: AccountSerialize, +{ + fn as_ref(&self) -> &AccountInfo<'info> { + self.info + } +} + +impl Key for Migration<'_, From, To> +where + From: AccountDeserialize, + To: AccountSerialize, +{ + fn key(&self) -> Pubkey { + *self.info.key + } +} + +// Deref to From when account is in Old state +impl<'info, From, To> Deref for Migration<'info, From, To> +where + From: AccountDeserialize, + To: AccountSerialize, +{ + type Target = From; + + fn deref(&self) -> &Self::Target { + match &self.inner { + MigrationInner::From(from) => from, + MigrationInner::To(_) => { + crate::solana_program::msg!("Cannot deref to From: account is already migrated."); + panic!(); + } + } + } +} + +// DerefMut to From when account is in Old state +impl<'info, From, To> DerefMut for Migration<'info, From, To> +where + From: AccountDeserialize, + To: AccountSerialize, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + match &mut self.inner { + MigrationInner::From(from) => from, + MigrationInner::To(_) => { + crate::solana_program::msg!( + "Cannot deref_mut to From: account is already migrated." + ); + panic!(); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AnchorDeserialize, AnchorSerialize, Discriminator}; + + const TEST_DISCRIMINATOR_V1: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; + const TEST_DISCRIMINATOR_V2: [u8; 8] = [8, 7, 6, 5, 4, 3, 2, 1]; + const TEST_OWNER: Pubkey = Pubkey::new_from_array([1u8; 32]); + + #[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, PartialEq)] + struct AccountV1 { + pub data: u64, + } + + impl Discriminator for AccountV1 { + const DISCRIMINATOR: &'static [u8] = &TEST_DISCRIMINATOR_V1; + } + + impl Owner for AccountV1 { + fn owner() -> Pubkey { + TEST_OWNER + } + } + + impl AccountSerialize for AccountV1 { + fn try_serialize(&self, writer: &mut W) -> Result<()> { + writer.write_all(&TEST_DISCRIMINATOR_V1)?; + AnchorSerialize::serialize(self, writer)?; + Ok(()) + } + } + + impl AccountDeserialize for AccountV1 { + fn try_deserialize(buf: &mut &[u8]) -> Result { + if buf.len() < 8 { + return Err(ErrorCode::AccountDiscriminatorNotFound.into()); + } + let disc = &buf[..8]; + if disc != TEST_DISCRIMINATOR_V1 { + return Err(ErrorCode::AccountDiscriminatorMismatch.into()); + } + Self::try_deserialize_unchecked(buf) + } + + fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result { + let mut data = &buf[8..]; + AnchorDeserialize::deserialize(&mut data) + .map_err(|_| ErrorCode::AccountDidNotDeserialize.into()) + } + } + + #[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, PartialEq)] + struct AccountV2 { + pub data: u64, + pub new_field: u64, + } + + impl Discriminator for AccountV2 { + const DISCRIMINATOR: &'static [u8] = &TEST_DISCRIMINATOR_V2; + } + + impl Owner for AccountV2 { + fn owner() -> Pubkey { + TEST_OWNER + } + } + + impl AccountSerialize for AccountV2 { + fn try_serialize(&self, writer: &mut W) -> Result<()> { + writer.write_all(&TEST_DISCRIMINATOR_V2)?; + AnchorSerialize::serialize(self, writer)?; + Ok(()) + } + } + + impl AccountDeserialize for AccountV2 { + fn try_deserialize(buf: &mut &[u8]) -> Result { + if buf.len() < 8 { + return Err(ErrorCode::AccountDiscriminatorNotFound.into()); + } + let disc = &buf[..8]; + if disc != TEST_DISCRIMINATOR_V2 { + return Err(ErrorCode::AccountDiscriminatorMismatch.into()); + } + Self::try_deserialize_unchecked(buf) + } + + fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result { + let mut data = &buf[8..]; + AnchorDeserialize::deserialize(&mut data) + .map_err(|_| ErrorCode::AccountDidNotDeserialize.into()) + } + } + + fn create_account_info<'a>( + key: &'a Pubkey, + owner: &'a Pubkey, + lamports: &'a mut u64, + data: &'a mut [u8], + ) -> AccountInfo<'a> { + AccountInfo::new(key, false, true, lamports, data, owner, false) + } + + // Verifies that a freshly deserialized Migration account reports + // is_migrated() as false, since it starts in the From state. + #[test] + fn test_is_migrated_returns_false_initially() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let migration: Migration = Migration::try_from(&info).unwrap(); + + assert!(!migration.is_migrated()); + } + + // Verifies that after calling migrate(), the account correctly + // reports is_migrated() as true. + #[test] + fn test_is_migrated_returns_true_after_migrate() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let mut migration: Migration = Migration::try_from(&info).unwrap(); + + migration + .migrate(AccountV2 { + data: 42, + new_field: 100, + }) + .unwrap(); + + assert!(migration.is_migrated()); + } + + // Verifies that try_as_from() successfully returns a reference to the + // old account data before migration has occurred. + #[test] + fn test_try_as_from_returns_data_before_migration() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let migration: Migration = Migration::try_from(&info).unwrap(); + + let from = migration.try_as_from().unwrap(); + assert_eq!(from.data, 42); + } + + // Verifies that try_as_from() returns an error after migration, + // providing a safe alternative to Deref that won't panic. + #[test] + fn test_try_as_from_returns_error_after_migration() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let mut migration: Migration = Migration::try_from(&info).unwrap(); + + migration + .migrate(AccountV2 { + data: 42, + new_field: 100, + }) + .unwrap(); + + assert!(migration.try_as_from().is_err()); + } + + // Verifies that try_as_from_mut() allows mutable access to the old + // account data before migration, and changes are persisted. + #[test] + fn test_try_as_from_mut_works_before_migration() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let mut migration: Migration = Migration::try_from(&info).unwrap(); + + let from = migration.try_as_from_mut().unwrap(); + from.data = 100; + assert_eq!(migration.try_as_from().unwrap().data, 100); + } + + // Verifies that calling migrate() twice returns an error, + // preventing accidental double-migration. + #[test] + fn test_migrate_fails_if_already_migrated() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let mut migration: Migration = Migration::try_from(&info).unwrap(); + + migration + .migrate(AccountV2 { + data: 42, + new_field: 100, + }) + .unwrap(); + let result = migration.migrate(AccountV2 { + data: 42, + new_field: 200, + }); + + assert!(result.is_err()); + } + + // Verifies that into_inner() performs migration and returns a + // reference to the new account data in a single call. + #[test] + fn test_into_inner_migrates_and_returns_reference() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let mut migration: Migration = Migration::try_from(&info).unwrap(); + + let to = migration.into_inner(AccountV2 { + data: 42, + new_field: 100, + }); + + assert_eq!(to.data, 42); + assert_eq!(to.new_field, 100); + assert!(migration.is_migrated()); + } + + // Verifies that into_inner() is idempotent - calling it multiple times + // returns the existing migrated data and ignores subsequent new_data arguments. + #[test] + fn test_into_inner_is_idempotent() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let mut migration: Migration = Migration::try_from(&info).unwrap(); + + let to1 = migration.into_inner(AccountV2 { + data: 42, + new_field: 100, + }); + assert_eq!(to1.new_field, 100); + + // Second call should return existing value, not use the new data + let to2 = migration.into_inner(AccountV2 { + data: 42, + new_field: 999, + }); + assert_eq!(to2.new_field, 100); // Still 100, not 999 + } + + // Verifies that into_inner_mut() returns a mutable reference, + // allowing modification of the migrated account data. + #[test] + fn test_into_inner_mut_allows_mutation() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let mut migration: Migration = Migration::try_from(&info).unwrap(); + + let to = migration.into_inner_mut(AccountV2 { + data: 42, + new_field: 100, + }); + to.new_field = 200; + + let to_ref = migration.into_inner(AccountV2 { + data: 0, + new_field: 0, + }); + assert_eq!(to_ref.new_field, 200); + } + + // Verifies that Deref allows direct field access (e.g., account.data) + // before migration has occurred. + #[test] + fn test_deref_works_before_migration() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let migration: Migration = Migration::try_from(&info).unwrap(); + + assert_eq!(migration.data, 42); + } + + // Verifies that Deref panics after migration. This documents the current + // behavior - use try_as_from() for safe access that returns Result instead. + #[test] + #[should_panic] + fn test_deref_panics_after_migration() { + let key = Pubkey::default(); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &TEST_OWNER, &mut lamports, &mut data); + let mut migration: Migration = Migration::try_from(&info).unwrap(); + + migration + .migrate(AccountV2 { + data: 42, + new_field: 100, + }) + .unwrap(); + + // This should panic + let _ = migration.data; + } + + // Verifies that deserialization fails when the account owner doesn't + // match the expected program, preventing unauthorized access. + #[test] + fn test_try_from_fails_with_wrong_owner() { + let key = Pubkey::default(); + let wrong_owner = Pubkey::new_from_array([99u8; 32]); + let mut lamports = 100; + let v1 = AccountV1 { data: 42 }; + let mut data = vec![0u8; 100]; + data[..8].copy_from_slice(&TEST_DISCRIMINATOR_V1); + v1.serialize(&mut &mut data[8..]).unwrap(); + + let info = create_account_info(&key, &wrong_owner, &mut lamports, &mut data); + let result: Result> = Migration::try_from(&info); + + assert!(result.is_err()); + } + + // Verifies that deserialization fails for uninitialized accounts + // (owned by system program with zero lamports). + #[test] + fn test_try_from_fails_with_uninitialized_account() { + let key = Pubkey::default(); + let mut lamports = 0; + let mut data = vec![0u8; 100]; + + let info = create_account_info(&key, &system_program::ID, &mut lamports, &mut data); + let result: Result> = Migration::try_from(&info); + + assert!(result.is_err()); + } +} diff --git a/lang/src/accounts/mod.rs b/lang/src/accounts/mod.rs index 7f92e3cd15..f18cd1313d 100644 --- a/lang/src/accounts/mod.rs +++ b/lang/src/accounts/mod.rs @@ -6,6 +6,7 @@ pub mod account_loader; pub mod boxed; pub mod interface; pub mod interface_account; +pub mod migration; pub mod option; pub mod program; pub mod signer; diff --git a/lang/src/error.rs b/lang/src/error.rs index 5149484091..dd5bdaa42c 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -192,6 +192,14 @@ pub enum ErrorCode { #[msg("Signature verification failed")] SignatureVerificationFailed, + // Migration errors + /// 2048 - Account is already migrated + #[msg("Account is already migrated")] + AccountAlreadyMigrated, + /// 2049 - Account must be migrated before exiting + #[msg("Account must be migrated before exiting")] + AccountNotMigrated, + // Require /// 2500 - A require expression was violated #[msg("A require expression was violated")] diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 7c216c65d5..4de12b461c 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -488,12 +488,13 @@ pub mod prelude { pub use super::{ access_control, account, accounts::account::Account, accounts::account_loader::AccountLoader, accounts::interface::Interface, - accounts::interface_account::InterfaceAccount, accounts::program::Program, - accounts::signer::Signer, accounts::system_account::SystemAccount, - accounts::sysvar::Sysvar, accounts::unchecked_account::UncheckedAccount, constant, - context::Context, context::CpiContext, declare_id, declare_program, emit, err, error, - event, instruction, program, pubkey, require, require_eq, require_gt, require_gte, - require_keys_eq, require_keys_neq, require_neq, + accounts::interface_account::InterfaceAccount, accounts::migration::Migration, + accounts::program::Program, accounts::signer::Signer, + accounts::system_account::SystemAccount, accounts::sysvar::Sysvar, + accounts::unchecked_account::UncheckedAccount, constant, context::Context, + context::CpiContext, declare_id, declare_program, emit, err, error, event, instruction, + program, pubkey, require, require_eq, require_gt, require_gte, require_keys_eq, + require_keys_neq, require_neq, solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source, system_program::System, zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Discriminator, Id, diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 8c860c0f89..6a3b307862 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -358,6 +358,13 @@ impl Field { } } } + Ty::Migration(ty) => { + let from = &ty.from_type_path; + let to = &ty.to_type_path; + quote! { + #container_ty<'info, #from, #to> + } + } _ => quote! { #container_ty<#account_ty> }, @@ -487,6 +494,9 @@ impl Field { Ty::AccountLoader(_) => quote! { anchor_lang::accounts::account_loader::AccountLoader }, + Ty::Migration(_) => quote! { + anchor_lang::accounts::migration::Migration + }, Ty::Sysvar(_) => quote! { anchor_lang::accounts::sysvar::Sysvar }, Ty::Program(_) => quote! { anchor_lang::accounts::program::Program }, Ty::Interface(_) => quote! { anchor_lang::accounts::interface::Interface }, @@ -543,6 +553,13 @@ impl Field { #ident } } + Ty::Migration(ty) => { + // Return just the From type for IDL and other uses + let from = &ty.from_type_path; + quote! { + #from + } + } Ty::Sysvar(ty) => match ty { SysvarTy::Clock => quote! {Clock}, SysvarTy::Rent => quote! {Rent}, @@ -596,6 +613,7 @@ pub enum Ty { Sysvar(SysvarTy), Account(AccountTy), LazyAccount(LazyAccountTy), + Migration(MigrationTy), Program(ProgramTy), Interface(InterfaceTy), InterfaceAccount(InterfaceAccountTy), @@ -638,6 +656,13 @@ pub struct LazyAccountTy { pub account_type_path: TypePath, } +#[derive(Debug, PartialEq, Eq)] +pub struct MigrationTy { + // Migration<'info, From, To> - we need both From and To types + pub from_type_path: TypePath, + pub to_type_path: TypePath, +} + #[derive(Debug, PartialEq, Eq)] pub struct InterfaceAccountTy { // The struct type of the account. diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index f804007182..e4333c40cf 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -1195,10 +1195,11 @@ impl<'ty> ConstraintGroupBuilder<'ty> { if !matches!(self.f_ty, Some(Ty::Account(_))) && !matches!(self.f_ty, Some(Ty::LazyAccount(_))) && !matches!(self.f_ty, Some(Ty::AccountLoader(_))) + && !matches!(self.f_ty, Some(Ty::Migration(_))) { return Err(ParseError::new( c.span(), - "realloc must be on an Account, LazyAccount or AccountLoader", + "realloc must be on an Account, LazyAccount, AccountLoader, or Migration", )); } if self.mutable.is_none() { diff --git a/lang/syn/src/parser/accounts/mod.rs b/lang/syn/src/parser/accounts/mod.rs index 83f82d60d8..db8340d4ed 100644 --- a/lang/syn/src/parser/accounts/mod.rs +++ b/lang/syn/src/parser/accounts/mod.rs @@ -349,6 +349,7 @@ fn is_field_primitive(f: &syn::Field) -> ParseResult { | "AccountLoader" | "Account" | "LazyAccount" + | "Migration" | "Program" | "Interface" | "InterfaceAccount" @@ -368,6 +369,7 @@ fn parse_ty(f: &syn::Field) -> ParseResult<(Ty, bool)> { "AccountLoader" => Ty::AccountLoader(parse_program_account_loader(&path)?), "Account" => Ty::Account(parse_account_ty(&path)?), "LazyAccount" => Ty::LazyAccount(parse_lazy_account_ty(&path)?), + "Migration" => Ty::Migration(parse_migration_ty(&path)?), "Program" => Ty::Program(parse_program_ty(&path)?), "Interface" => Ty::Interface(parse_interface_ty(&path)?), "InterfaceAccount" => Ty::InterfaceAccount(parse_interface_account_ty(&path)?), @@ -465,6 +467,49 @@ fn parse_lazy_account_ty(path: &syn::Path) -> ParseResult { Ok(LazyAccountTy { account_type_path }) } +fn parse_migration_ty(path: &syn::Path) -> ParseResult { + // Migration<'info, From, To> + let segments = &path.segments[0]; + match &segments.arguments { + syn::PathArguments::AngleBracketed(args) => { + // Expected: <'info, From, To> - 3 args + if args.args.len() != 3 { + return Err(ParseError::new( + args.args.span(), + "Migration requires three arguments: lifetime, From type, and To type", + )); + } + // First arg is lifetime, second is From, third is To + let from_type_path = match &args.args[1] { + syn::GenericArgument::Type(syn::Type::Path(ty_path)) => ty_path.clone(), + _ => { + return Err(ParseError::new( + args.args[1].span(), + "From type must be a path", + )); + } + }; + let to_type_path = match &args.args[2] { + syn::GenericArgument::Type(syn::Type::Path(ty_path)) => ty_path.clone(), + _ => { + return Err(ParseError::new( + args.args[2].span(), + "To type must be a path", + )); + } + }; + Ok(MigrationTy { + from_type_path, + to_type_path, + }) + } + _ => Err(ParseError::new( + segments.span(), + "Migration must have angle bracketed arguments", + )), + } +} + fn parse_interface_account_ty(path: &syn::Path) -> ParseResult { let account_type_path = parse_account(path)?; let boxed = parser::tts_to_string(path)