如何升级Solana程序
Solana程序基于链上的账户,当程序升级时,如果只设计算法,不涉及账户(PDA等)中数据结构的修改,无需升级账户。但是如果涉及到PDA账户数据结构的改变,则情况变得复杂。
示例
在下面的例子中,我们定义了一个StateAccount
的PDA账户,先编译并执行该程序,没有问题。
lib.rs
use anchor_lang::prelude::*;
declare_id!("5QnLU9oAWsY2tVX8Yw9BhcaHgJTnQm9vG7rmLGu2YS2F");
#[program]
pub mod upgradeable_program {
use super::*;
// 初始化状态账户
pub fn initialize(ctx: Context<Initialize>, initial_value: u64) -> Result<()> {
let state = &mut ctx.accounts.state;
state.value = initial_value;
state.authority = ctx.accounts.authority.key();
emit!(LogEvent {
data: initial_value,
timestamp: Clock::get()?.unix_timestamp,
});
Ok(())
}
// 更新状态账户
pub fn update_state(ctx: Context<UpdateState>, new_value: u64) -> Result<()> {
let state = &mut ctx.accounts.state;
state.value = new_value;
emit!(LogEvent {
data: new_value,
timestamp: Clock::get()?.unix_timestamp,
});
Ok(())
}
}
// 账户结构体
#[account]
#[derive(InitSpace)]
pub struct StateAccount {
pub value: u64, // 初始值
pub authority: Pubkey, // 权限控制账户
}
#[event]
pub struct LogEvent {
pub data: u64, // 事件数据
pub timestamp: i64, // 时间戳
}
// 上下文结构体
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
seeds = [b"state".as_ref()],
bump,
init_if_needed,
payer = authority,
space = 8 + StateAccount::INIT_SPACE
)]
pub state: Account<'info, StateAccount>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateState<'info> {
#[account(mut, has_one = authority)]
pub state: Account<'info, StateAccount>,
pub authority: Signer<'info>,
}
upgradeable-program.ts测试脚本
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { UpgradeableProgram } from "../target/types/upgradeable_program";
describe("upgradeable-program", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.UpgradeableProgram as Program<UpgradeableProgram>;
const owner = provider.wallet;
const [statePda] = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("state"),
],
program.programId
);
it("Initialize state", async () => {
const stateInfo = await provider.connection.getAccountInfo(statePda);
if (stateInfo) {
console.log("state account existed");
return;
}
const context = {
state: statePda,
authority: owner.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
}
await program.methods
.initialize(new anchor.BN(100))
.accounts(context)
.rpc();
const state = await program.account.stateAccount.fetch(statePda);
console.log("Initialized State:", state.value.toNumber());
});
it("Update state", async () => {
const context = {
state: statePda,
authority: owner.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
}
await program.methods
.updateState(new anchor.BN(42))
.accounts(context)
.rpc();
const state = await program.account.stateAccount.fetch(statePda);
console.log("Updated State:", state.value.toNumber());
});
});
编译,部署并执行上面程序,执行正常。
升级程序账户
现在将StateAccount
的账户升级下,增加新的字段:
pub struct StateAccount {
pub value: u64,
pub authority: Pubkey,
pub extra_field: u32, // 新增字段
}
相应的,在initialize
和update_state
函数中改动如下:
pub fn initialize(ctx: Context<Initialize>, initial_value: u64) -> Result<()> {
let state = &mut ctx.accounts.state;
state.value = initial_value;
state.authority = ctx.accounts.authority.key();
state.extra_field = 0;
emit!(LogEvent {
data: initial_value,
timestamp: Clock::get()?.unix_timestamp,
description: String::from("initialize"),
});
Ok(())
}
// 更新状态账户
pub fn update_state(ctx: Context<UpdateState>, new_value: u64) -> Result<()> {
let state = &mut ctx.accounts.state;
state.value = new_value;
state.extra_field = 10;
emit!(LogEvent {
data: new_value,
timestamp: Clock::get()?.unix_timestamp,
description: String::from("update_state"),
});
Ok(())
}
将新的lib.rs
编译并部署后,再次执行,会出错:
Error: AnchorError caused by account: state. Error Code: AccountDidNotDeserialize. Error Number: 3003. Error Message: Failed to deserialize the account.
因为StateAccount
账户数据结构的变化,导致无法正常解析。现在该怎么办呢?
Solana官方有个教程可以参考:https://solanacookbook.com/guides/data-migration.html#how-can-you-migrate-a-program-s-data-accounts
这里笔者介绍一个简单的方法:通过修改seeds
改变账户,方法如下:
1- 修改初始化指令中的账户中的seeds
把seeds从b"state"
修改为b"state_v1"
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
seeds = [b"state_v1".as_ref()],
bump,
init_if_needed,
payer = authority,
space = 8 + StateAccount::INIT_SPACE
)]
pub state: Account<'info, StateAccount>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
2- 在前端调试脚本中,做相应调整:
const [statePda] = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("state_v1"),
],
program.programId
);
这样就把旧的账户废了,换成了新的账户。
但是,如果需要同步旧账户中的数据到新的账户,需要写javascript
脚本转。