迁移程序的数据账户
你如何迁移一个程序的数据账户?
当你创建一个程序时,与该程序关联的每个数据账户都将具有特定的数据结构。如果你需要升级一个程序派生账户,那么你将得到一堆具有旧结构的剩余程序派生账户。
通过账户版本控制,您可以将旧账户升级到新的结构。
注意
这只是在程序拥有的账户(POA)中迁移数据的众多方法之一。
场景
为了对账户数据进行版本控制和迁移,我们将为每个账户提供一个ID。该ID允许我们在将其传递给程序时识别账户的版本,从而正确处理账户。
假设有以下账户状态和程序:
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(),
})
}
}
}
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct AccountContentCurrent {
pub somevalue: u64,
}
#[derive(BorshDeserialize, BorshSerialize, Debug, Default, PartialEq)]
pub struct ProgramAccountState {
is_initialized: bool,
data_version: u8,
account_data: AccountContentCurrent,
}
//! 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()),
}
}
}
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())
}
}
}
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())
}
}
}
在我们账户的第一个版本中,我们执行以下操作:
ID | Action |
---|---|
1 | Include a 'data version' field in your data. It can be a simple incrementing ordinal (e.g. u8) or something more sophisticated |
2 | Allocating enough space for data growth |
3 | Initializing a number of constants to be used across program versions |
4 | Add 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(),
})
}
}
}
/// 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,
}
Line(s) | Note |
---|---|
6 | We've added Solana's solana_program::borsh::try_from_slice_unchecked to simplify reading subsets of data from the larger data block |
13-26 | Here we've preserved the old content structure, AccountContentOld line 24, before extending the AccountContentCurrent starting in line 17. |
60 | We bump the DATA_VERSION constant |
71 | We now have a 'previous' version and we want to know it's size |
86 | The 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)
后,我们现在有了以下 "升级" 的账户数据布局。