Jacky Gu

Solana开发笔记: 如何升级Solana程序

01 Jan 2025 Share to

如何升级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,   // 新增字段
}

相应的,在initializeupdate_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脚本转。