迁移程序的数据账户

你如何迁移一个程序的数据账户?

当你创建一个程序时,与该程序关联的每个数据账户都将具有特定的数据结构。如果你需要升级一个程序派生账户,那么你将得到一堆具有旧结构的剩余程序派生账户。

通过账户版本控制,您可以将旧账户升级到新的结构。

注意

这只是在程序拥有的账户(POA)中迁移数据的众多方法之一。

场景

为了对账户数据进行版本控制和迁移,我们将为每个账户提供一个ID。该ID允许我们在将其传递给程序时识别账户的版本,从而正确处理账户。

假设有以下账户状态和程序:

Program Account v1
Press </> button to view full source
//! @brief account_state manages account data

use arrayref::{array_ref, array_refs};
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    msg,
    program_error::ProgramError,
    program_pack::{IsInitialized, Pack, Sealed},
};
use std::{io::BufWriter, mem};

/// Currently using state. If version changes occur, this
/// should be copied to another serializable backlevel one
/// before adding new fields here
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct AccountContentCurrent {
    pub somevalue: u64,
}

/// Maintains account data
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct ProgramAccountState {
    is_initialized: bool,
    data_version: u8,
    account_data: AccountContentCurrent,
}

impl ProgramAccountState {
    /// Signal initialized
    pub fn set_initialized(&mut self) {
        self.is_initialized = true;
    }
    /// Get the initialized flag
    pub fn initialized(&self) -> bool {
        self.is_initialized
    }
    /// Gets the current data version
    pub fn version(&self) -> u8 {
        self.data_version
    }
    /// Get the reference to content structure
    pub fn content(&self) -> &AccountContentCurrent {
        &self.account_data
    }
    /// Get the mutable reference to content structure
    pub fn content_mut(&mut self) -> &mut AccountContentCurrent {
        &mut self.account_data
    }
}

/// Declaration of the current data version.
pub const DATA_VERSION: u8 = 0;
/// Account allocated size
pub const ACCOUNT_ALLOCATION_SIZE: usize = 1024;
/// Initialized flag is 1st byte of data block
const IS_INITIALIZED: usize = 1;
/// Data version (current) is 2nd byte of data block
const DATA_VERSION_ID: usize = 1;

/// Previous content data size (before changing this is equal to current)
pub const PREVIOUS_VERSION_DATA_SIZE: usize = mem::size_of::<AccountContentCurrent>();
/// Total space occupied by previous account data
pub const PREVIOUS_ACCOUNT_SPACE: usize =
    IS_INITIALIZED + DATA_VERSION_ID + PREVIOUS_VERSION_DATA_SIZE;

/// Current content data size
pub const CURRENT_VERSION_DATA_SIZE: usize = mem::size_of::<AccountContentCurrent>();
/// Total usage for data only
pub const CURRENT_USED_SIZE: usize = IS_INITIALIZED + DATA_VERSION_ID + CURRENT_VERSION_DATA_SIZE;
/// How much of 1024 is used
pub const CURRENT_UNUSED_SIZE: usize = ACCOUNT_ALLOCATION_SIZE - CURRENT_USED_SIZE;
/// Current space used by header (initialized, data version and Content)
pub const ACCOUNT_STATE_SPACE: usize = CURRENT_USED_SIZE + CURRENT_UNUSED_SIZE;

/// Future data migration logic that converts prior state of data
/// to current state of data
fn conversion_logic(src: &[u8]) -> Result<ProgramAccountState, ProgramError> {
    let past = array_ref![src, 0, PREVIOUS_ACCOUNT_SPACE];
    let (initialized, _, _account_space) = array_refs![
        past,
        IS_INITIALIZED,
        DATA_VERSION_ID,
        PREVIOUS_VERSION_DATA_SIZE
    ];
    // Logic to uplift from previous version
    // GOES HERE

    // Give back
    Ok(ProgramAccountState {
        is_initialized: initialized[0] != 0u8,
        data_version: DATA_VERSION,
        account_data: AccountContentCurrent::default(),
    })
}
impl Sealed for ProgramAccountState {}

impl IsInitialized for ProgramAccountState {
    fn is_initialized(&self) -> bool {
        self.is_initialized
    }
}

impl Pack for ProgramAccountState {
    const LEN: usize = ACCOUNT_STATE_SPACE;

    /// Store 'state' of account to its data area
    fn pack_into_slice(&self, dst: &mut [u8]) {
        let mut bw = BufWriter::new(dst);
        self.serialize(&mut bw).unwrap();
    }

    /// Retrieve 'state' of account from account data area
    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
        let initialized = src[0] != 0;
        // Check initialized
        if initialized {
            // Version check
            if src[1] == DATA_VERSION {
                msg!("Processing consistent data");
                Ok(
                    ProgramAccountState::try_from_slice(array_ref![src, 0, CURRENT_USED_SIZE])
                        .unwrap(),
                )
            } else {
                msg!("Processing backlevel data");
                conversion_logic(src)
            }
        } else {
            msg!("Processing pre-initialized data");
            Ok(ProgramAccountState {
                is_initialized: false,
                data_version: DATA_VERSION,
                account_data: AccountContentCurrent::default(),
            })
        }
    }
}
//! instruction Contains the main ProgramInstruction enum

use {
    crate::error::DataVersionError,
    borsh::{BorshDeserialize, BorshSerialize},
    solana_program::program_error::ProgramError,
};

#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)]
/// All custom program instructions
pub enum ProgramInstruction {
    InitializeAccount,
    SetU64Value(u64),
    FailInstruction,
}

impl ProgramInstruction {
    /// Unpack inbound buffer to associated Instruction
    /// The expected format for input is a Borsh serialized vector
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
        let payload = ProgramInstruction::try_from_slice(input).unwrap();
        match payload {
            ProgramInstruction::InitializeAccount => Ok(payload),
            ProgramInstruction::SetU64Value(_) => Ok(payload),
            _ => Err(DataVersionError::InvalidInstruction.into()),
        }
    }
}
//! Resolve instruction and execute

use crate::{
    account_state::ProgramAccountState, error::DataVersionError, instruction::ProgramInstruction,
};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    program_pack::{IsInitialized, Pack},
    pubkey::Pubkey,
};

/// Checks each tracking account to confirm it is owned by our program
/// This function assumes that the program account is always the last
/// in the array
fn check_account_ownership(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    // Accounts must be owned by the program.
    for account in accounts.iter().take(accounts.len() - 1) {
        if account.owner != program_id {
            msg!(
                "Fail: The tracking account owner is {} and it should be {}.",
                account.owner,
                program_id
            );
            return Err(ProgramError::IncorrectProgramId);
        }
    }
    Ok(())
}

/// Initialize the programs account, which is the first in accounts
fn initialize_account(accounts: &[AccountInfo]) -> ProgramResult {
    msg!("Initialize account");
    let account_info_iter = &mut accounts.iter();
    let program_account = next_account_info(account_info_iter)?;
    let mut account_data = program_account.data.borrow_mut();
    // Just using unpack will check to see if initialized and will
    // fail if not
    let mut account_state = ProgramAccountState::unpack_unchecked(&account_data)?;
    // Where this is a logic error in trying to initialize the same account more than once
    if account_state.is_initialized() {
        return Err(DataVersionError::AlreadyInitializedState.into());
    } else {
        account_state.set_initialized();
        account_state.content_mut().somevalue = 1;
    }
    msg!("Account Initialized");
    // Serialize
    ProgramAccountState::pack(account_state, &mut account_data)
}

/// Sets the u64 in the content structure
fn set_u64_value(accounts: &[AccountInfo], value: u64) -> ProgramResult {
    msg!("Set new value {}", value);
    let account_info_iter = &mut accounts.iter();
    let program_account = next_account_info(account_info_iter)?;
    let mut account_data = program_account.data.borrow_mut();
    let mut account_state = ProgramAccountState::unpack(&account_data)?;
    account_state.content_mut().somevalue = value;
    // Serialize
    ProgramAccountState::pack(account_state, &mut account_data)
}
/// Main processing entry point dispatches to specific
/// instruction handlers
pub fn process(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    msg!("Received process request");
    // Check the account for program relationship
    if let Err(error) = check_account_ownership(program_id, accounts) {
        return Err(error);
    };
    // Unpack the inbound data, mapping instruction to appropriate structure
    let instruction = ProgramInstruction::unpack(instruction_data)?;
    match instruction {
        ProgramInstruction::InitializeAccount => initialize_account(accounts),
        ProgramInstruction::SetU64Value(value) => set_u64_value(accounts, value),
        _ => {
            msg!("Received unknown instruction");
            Err(DataVersionError::InvalidInstruction.into())
        }
    }
}

在我们账户的第一个版本中,我们执行以下操作:

IDAction
1Include a 'data version' field in your data. It can be a simple incrementing ordinal (e.g. u8) or something more sophisticated
2Allocating enough space for data growth
3Initializing a number of constants to be used across program versions
4Add an update account function under fn conversion_logic for future upgrades

假设我们现在希望升级程序的账户,包括一个新的必需字段:somestring字段。

如果我们之前没有为账户分配额外的空间,我们将无法升级该账户,而被卡住。

升级账户

在我们的新程序中,我们希望为内容状态添加一个新属性。下面的变化展示了我们如何利用初始的程序结构,并在现在使用时进行修改。

1. 添加账户转换逻辑

Press </> button to view full source
//! @brief account_state manages account data

use arrayref::{array_ref, array_refs};
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    borsh::try_from_slice_unchecked,
    msg,
    program_error::ProgramError,
    program_pack::{IsInitialized, Pack, Sealed},
};
use std::{io::BufWriter, mem};

/// Current state (DATA_VERSION 1). If version changes occur, this
/// should be copied to another (see AccountContentOld below)
/// We've added a new field: 'somestring'
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct AccountContentCurrent {
    pub somevalue: u64,
    pub somestring: String,
}

/// Old content state (DATA_VERSION 0).
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct AccountContentOld {
    pub somevalue: u64,
}

/// Maintains account data
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct ProgramAccountState {
    is_initialized: bool,
    data_version: u8,
    account_data: AccountContentCurrent,
}

impl ProgramAccountState {
    /// Signal initialized
    pub fn set_initialized(&mut self) {
        self.is_initialized = true;
    }
    /// Get the initialized flag
    pub fn initialized(&self) -> bool {
        self.is_initialized
    }
    /// Gets the current data version
    pub fn version(&self) -> u8 {
        self.data_version
    }
    /// Get the reference to content structure
    pub fn content(&self) -> &AccountContentCurrent {
        &self.account_data
    }
    /// Get the mutable reference to content structure
    pub fn content_mut(&mut self) -> &mut AccountContentCurrent {
        &mut self.account_data
    }
}

/// Declaration of the current data version.
const DATA_VERSION: u8 = 1; // Adding string to content
                            // Previous const DATA_VERSION: u8 = 0;

/// Account allocated size
const ACCOUNT_ALLOCATION_SIZE: usize = 1024;
/// Initialized flag is 1st byte of data block
const IS_INITIALIZED: usize = 1;
/// Data version (current) is 2nd byte of data block
const DATA_VERSION_ID: usize = 1;

/// Previous content data size (before changing this is equal to current)
const PREVIOUS_VERSION_DATA_SIZE: usize = mem::size_of::<AccountContentOld>();
/// Total space occupied by previous account data
const PREVIOUS_ACCOUNT_SPACE: usize = IS_INITIALIZED + DATA_VERSION_ID + PREVIOUS_VERSION_DATA_SIZE;

/// Current content data size
const CURRENT_VERSION_DATA_SIZE: usize = mem::size_of::<AccountContentCurrent>();
/// Total usage for data only
const CURRENT_USED_SIZE: usize = IS_INITIALIZED + DATA_VERSION_ID + CURRENT_VERSION_DATA_SIZE;
/// How much of 1024 is used
const CURRENT_UNUSED_SIZE: usize = ACCOUNT_ALLOCATION_SIZE - CURRENT_USED_SIZE;
/// Current space used by header (initialized, data version and Content)
pub const ACCOUNT_STATE_SPACE: usize = CURRENT_USED_SIZE + CURRENT_UNUSED_SIZE;

/// Future data migration logic that converts prior state of data
/// to current state of data
fn conversion_logic(src: &[u8]) -> Result<ProgramAccountState, ProgramError> {
    let past = array_ref![src, 0, PREVIOUS_ACCOUNT_SPACE];
    let (initialized, _, account_space) = array_refs![
        past,
        IS_INITIALIZED,
        DATA_VERSION_ID,
        PREVIOUS_VERSION_DATA_SIZE
    ];
    // Logic to upgrade from previous version
    // GOES HERE
    let old = try_from_slice_unchecked::<AccountContentOld>(account_space).unwrap();
    // Default sets 'somevalue' to 0 and somestring to default ""
    let mut new_content = AccountContentCurrent::default();
    // We copy the existing 'somevalue', the program instructions will read/update 'somestring' without fail
    new_content.somevalue = old.somevalue;

    // Give back
    Ok(ProgramAccountState {
        is_initialized: initialized[0] != 0u8,
        data_version: DATA_VERSION,
        account_data: new_content,
    })
}
impl Sealed for ProgramAccountState {}

impl IsInitialized for ProgramAccountState {
    fn is_initialized(&self) -> bool {
        self.is_initialized
    }
}

impl Pack for ProgramAccountState {
    const LEN: usize = ACCOUNT_STATE_SPACE;

    /// Store 'state' of account to its data area
    fn pack_into_slice(&self, dst: &mut [u8]) {
        let mut bw = BufWriter::new(dst);
        self.serialize(&mut bw).unwrap();
    }

    /// Retrieve 'state' of account from account data area
    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
        let initialized = src[0] != 0;
        // Check initialized
        if initialized {
            // Version check
            if src[1] == DATA_VERSION {
                msg!("Processing consistent version data");
                Ok(try_from_slice_unchecked::<ProgramAccountState>(src).unwrap())
            } else {
                msg!("Processing backlevel data");
                conversion_logic(src)
            }
        } else {
            msg!("Processing pre-initialized data");
            Ok(ProgramAccountState {
                is_initialized: false,
                data_version: DATA_VERSION,
                account_data: AccountContentCurrent::default(),
            })
        }
    }
}
Line(s)Note
6We've added Solana's solana_program::borsh::try_from_slice_unchecked to simplify reading subsets of data from the larger data block
13-26Here we've preserved the old content structure, AccountContentOld line 24, before extending the AccountContentCurrent starting in line 17.
60We bump the DATA_VERSION constant
71We now have a 'previous' version and we want to know it's size
86The Coup de grâce is adding the plumbing to upgrade the previous content state to the new (current) content state

然后,我们更新指令,添加一个新的指令来更新somestring,并更新处理器来处理新的指令。请注意,"升级"数据结构是通过pack/unpack封装起来的。

//! instruction Contains the main VersionProgramInstruction enum

use {
    crate::error::DataVersionError,
    borsh::{BorshDeserialize, BorshSerialize},
    solana_program::{borsh::try_from_slice_unchecked, msg, program_error::ProgramError},
};

#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)]
/// All custom program instructions
pub enum VersionProgramInstruction {
    InitializeAccount,
    SetU64Value(u64),
    SetString(String), // Added with data version change
    FailInstruction,
}

impl VersionProgramInstruction {
    /// Unpack inbound buffer to associated Instruction
    /// The expected format for input is a Borsh serialized vector
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
        let payload = try_from_slice_unchecked::<VersionProgramInstruction>(input).unwrap();
        // let payload = VersionProgramInstruction::try_from_slice(input).unwrap();
        match payload {
            VersionProgramInstruction::InitializeAccount => Ok(payload),
            VersionProgramInstruction::SetU64Value(_) => Ok(payload),
            VersionProgramInstruction::SetString(_) => Ok(payload), // Added with data version change
            _ => Err(DataVersionError::InvalidInstruction.into()),
        }
    }
}
//! Resolve instruction and execute

use crate::{
    account_state::ProgramAccountState, error::DataVersionError,
    instruction::VersionProgramInstruction,
};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    program_pack::{IsInitialized, Pack},
    pubkey::Pubkey,
};

/// Checks each tracking account to confirm it is owned by our program
/// This function assumes that the program account is always the last
/// in the array
fn check_account_ownership(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    // Accounts must be owned by the program.
    for account in accounts.iter().take(accounts.len() - 1) {
        if account.owner != program_id {
            msg!(
                "Fail: The tracking account owner is {} and it should be {}.",
                account.owner,
                program_id
            );
            return Err(ProgramError::IncorrectProgramId);
        }
    }
    Ok(())
}

/// Initialize the programs account, which is the first in accounts
fn initialize_account(accounts: &[AccountInfo]) -> ProgramResult {
    msg!("Initialize account");
    let account_info_iter = &mut accounts.iter();
    let program_account = next_account_info(account_info_iter)?;
    let mut account_data = program_account.data.borrow_mut();
    // Just using unpack will check to see if initialized and will
    // fail if not
    let mut account_state = ProgramAccountState::unpack_unchecked(&account_data)?;
    // Where this is a logic error in trying to initialize the same account more than once
    if account_state.is_initialized() {
        return Err(DataVersionError::AlreadyInitializedState.into());
    } else {
        account_state.set_initialized();
        account_state.content_mut().somevalue = 1;
    }
    msg!("Account Initialized");
    // Serialize
    ProgramAccountState::pack(account_state, &mut account_data)
}

/// Sets the u64 in the content structure
fn set_u64_value(accounts: &[AccountInfo], value: u64) -> ProgramResult {
    msg!("Set new value {}", value);
    let account_info_iter = &mut accounts.iter();
    let program_account = next_account_info(account_info_iter)?;
    let mut account_data = program_account.data.borrow_mut();
    let mut account_state = ProgramAccountState::unpack(&account_data)?;
    account_state.content_mut().somevalue = value;
    // Serialize
    ProgramAccountState::pack(account_state, &mut account_data)
}

/// Sets the string in the content structure
fn set_string_value(accounts: &[AccountInfo], value: String) -> ProgramResult {
    msg!("Set new string {}", value);
    let account_info_iter = &mut accounts.iter();
    let program_account = next_account_info(account_info_iter)?;
    let mut account_data = program_account.data.borrow_mut();
    let mut account_state = ProgramAccountState::unpack(&account_data)?;
    account_state.content_mut().somestring = value;
    // Serialize
    ProgramAccountState::pack(account_state, &mut account_data)
}
/// Main processing entry point dispatches to specific
/// instruction handlers
pub fn process(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    msg!("Received process request 0.2.0");
    // Check the account for program relationship
    if let Err(error) = check_account_ownership(program_id, accounts) {
        return Err(error);
    };
    // Unpack the inbound data, mapping instruction to appropriate structure
    msg!("Attempting to unpack");
    let instruction = VersionProgramInstruction::unpack(instruction_data)?;
    match instruction {
        VersionProgramInstruction::InitializeAccount => initialize_account(accounts),
        VersionProgramInstruction::SetU64Value(value) => set_u64_value(accounts, value),
        VersionProgramInstruction::SetString(value) => set_string_value(accounts, value),
        _ => {
            msg!("Received unknown instruction");
            Err(DataVersionError::InvalidInstruction.into())
        }
    }
}

在构建并提交指令VersionProgramInstruction::SetString(String)后,我们现在有了以下 "升级" 的账户数据布局。

Program Account v2

资料

Last Updated:
Contributors: lillianrf