Solana

2026年01月24日更新 17 人订阅
原价: ¥ 8.6 限时优惠
专栏简介 Solana 开发学习之Solana 基础知识 Solana 开发学习之通过RPC与Solana交互 Web3与Solana实操指南:如何签名与发送交易 Web3 新玩法:Solana Nonce Account 让你交易无忧 Web3 快上手:Solana 造你的链上名片 Web3 开发实战:用 Anchor 打造 Solana 猜数游戏 @solana/web3.js 2.0:Solana 转账全流程解析 玩转 Web3 Solana:从零到代币开发 Web3 开发入门:Solana CLI 配置与本地验证器实战 Web3 Eclipse 开发环境搭建与资产跨链桥接实战指南 用 Gill 库解锁 Web3:Solana 代币转账实战 Web3开发:用Rust实现Solana SOL转账教程 用 Rust 开发 Solana:解锁 Web3 交易费用计算 Web3开发入门:Solana账户创建与Rust实践全攻略 Web3 实战:用 Anchor 打造 Solana 智能合约全流程 Solana Web3 快速入门:创建并获取钱包账户的完整指南 Web3 开发实操:用 Anchor 在 Solana 创建代币 Mint Account 从零到 Web3:使用 @solana/kit 快速查询 Solana 账户余额 快速上手 Web3:用 @solana/kit 在 Solana 上创建钱包并查询余额 Web3实战:使用Anchor与Rust开发和调用Solana智能合约 Web3实战:Solana CPI全解析,从Anchor封装到PDA转账 用 Rust 在 Solana 上打造你的专属代币:从零到一的 Web3 实践 探索Solana SDK实战:Web3开发的双路径与轻量模块化 手把手教你用 Solana Token-2022 创建支持元数据的区块链代币 Solana 开发实战:Rust 客户端调用链上程序全流程 Solana 开发进阶:在 Devnet 上实现链上程序部署、调用与更新 Solana 开发进阶:链上事件到链下解析全攻略 从零打造Solana空投工具库:Rust开发实战指南 从零开始:用 Rust 开发 Solana 链上 Token 元数据查询工具 Solana 智能合约终极部署指南:从入门到主网,定制你的专属靓号 Program ID 【Solana 开发实战】轻松搞定链上 IDL:从上传到获取全解析 Solana 投票 DApp 开发实战:从合约到部署的完整指南 Surfpool:Solana 上的 Anvil,本地开发闪电般⚡️ 【Solana实操】64字节私钥文件解析难题:用三种姿势安全获取钱包地址 Solana 密钥实战:一文搞懂私钥、公钥、PDA 的底层关系与 CLI 操作 Solana 地址进阶:从 TS/JS 到 Rust SDK V3,完全掌握公钥与 PDA 的底层逻辑 Solana 开发者笔记:PDA 与账户操作的10个关键要点 拒绝“版本代差”:基于 Solana SDK V3 的「链上动态存储器」工业级实现 从零到 Devnet:Solana Anchor Vault 个人金库开发全流程实操 Anchor 中一个隐蔽但致命的坑:Accounts 顺序导致 AccountNotInitialized

Anchor 中一个隐蔽但致命的坑:Accounts 顺序导致 AccountNotInitialized

Anchor中一个隐蔽但致命的坑:Accounts顺序导致AccountNotInitialized结论先行:在[derive(Accounts)]中,账户字段的顺序会真实影响程序是否能正常运行。尤其是当你同时使用了associated_token+init_if_needed

Anchor 中一个隐蔽但致命的坑:Accounts 顺序导致 AccountNotInitialized

结论先行:在 #[derive(Accounts)] 中,账户字段的顺序会真实影响程序是否能正常运行。尤其是当你同时使用了 associated_token + init_if_needed + InterfaceAccount 时,顺序不当会直接导致 AccountNotInitialized (3012) 这类“看起来完全不合理”的错误

这篇文章记录一次真实的踩坑经历:代码逻辑完全正确、和别人几乎一模一样,却怎么都跑不通。最终发现,问题的根源竟然只是——账户结构体里字段的顺序


一、问题背景

在实现一个 Escrow 合约的 refund 指令时,逻辑非常简单:

  1. Maker 在无人 take 的情况下调用 refund
  2. 将 Vault 中的 Token A 转回给 Maker
  3. 关闭 Vault ATA
  4. 关闭 Escrow PDA

对应的账户包括:

  • maker(Signer)
  • escrow(PDA)
  • mint_a
  • vault(escrow 持有的 ATA)
  • maker_ata_a(maker 的 ATA,init_if_needed

逻辑本身非常标准,代码和社区示例几乎一致。


二、诡异的错误现象

在运行测试时,程序不断报错:

AnchorError caused by account: vault
Error Code: AccountNotInitialized (3012)
The program expected this account to be already initialized.

但问题在于:

  • vault 明明在 make 指令中已经创建
  • PDA / seed / authority 全部校验正确
  • 别人的 refund 代码,测试可以直接通过
  • 自己的代码,怎么改 handler 都不行

这类错误非常误导,很容易让人怀疑:

  • PDA seeds 写错了?
  • CPI signer 有问题?
  • Token Program / Token-2022 不一致?

但全部排查之后,仍然无解。


三、关键发现:Accounts 字段顺序不同

最终通过逐行对比,发现了一个唯一但关键的差异

❌ 出问题的写法

#[account(
    init_if_needed,
    payer = maker,
    associated_token::mint = mint_a,
    associated_token::authority = maker,
    associated_token::token_program = token_program
)]
pub maker_ata_a: InterfaceAccount<'info, TokenAccount>,

#[account(
    mut,
    associated_token::mint = mint_a,
    associated_token::authority = escrow,
    associated_token::token_program = token_program
)]
pub vault: InterfaceAccount<'info, TokenAccount>,

✅ 正确、可运行的写法

#[account(
    mut,
    associated_token::mint = mint_a,
    associated_token::authority = escrow,
    associated_token::token_program = token_program
)]
pub vault: InterfaceAccount<'info, TokenAccount>,

#[account(
    init_if_needed,
    payer = maker,
    associated_token::mint = mint_a,
    associated_token::authority = maker,
    associated_token::token_program = token_program
)]
pub maker_ata_a: InterfaceAccount<'info, TokenAccount>,

唯一的变化:把 vault 放在 maker_ata_a (init_if_needed) 前面。

结果:

  • ❌ 原来:必定报 AccountNotInitialized
  • ✅ 调整顺序后:测试全部通过

四、为什么顺序真的会影响?

1️⃣ Anchor 校验 Accounts 的方式

Anchor 在进入 handler 之前,会对 #[derive(Accounts)] 中的字段 按顺序 做两件事:

  1. 反序列化账户
  2. 执行约束(constraints)

伪代码如下:

for field in accounts_in_order {
    deserialize(field);
    run_constraints(field);
}

👉 不是整体校验,而是逐字段顺序执行。

2️⃣ init_if_needed 是“有副作用”的约束

#[account(init_if_needed, associated_token::...)]

这一条约束并不只是校验,它可能会:

  • 通过 CPI 调用 associated_token_program
  • 创建 ATA
  • 修改 lamports / owner / data

也就是说:

它会在 Accounts 校验阶段“改变当前交易的账户状态”


3️⃣ 顺序错误时发生了什么?

maker_ata_a (init_if_needed) 写在前面时:

  1. Anchor 先执行 init_if_needed
  2. 如果 ATA 不存在,立刻 CPI 创建
  3. Accounts 状态被修改
  4. 再校验 vault

InterfaceAccount + associated_token 的组合下, Anchor 会错误地将 vault 判断为“未初始化账户”,从而抛出:

AccountNotInitialized (3012)

这就是为什么错误信息看起来和真实原因完全不相关。


五、为什么别人的代码没问题?

原因很简单:

他们的 Accounts 顺序是“安全顺序”

即:

  1. 已存在账户(PDA / vault / escrow)
  2. 可能 init 的账户(maker ATA)
  3. program accounts

而不是反过来。


六、最终解决方案(可直接抄)

✅ 正确的 Accounts 顺序模板

#[derive(Accounts)]
pub struct Refund<'info> {
    #[account(mut)]
    pub maker: Signer<'info>,

    #[account(...)]
    pub escrow: Account<'info, Escrow>,

    pub mint_a: InterfaceAccount<'info, Mint>,

    // 1️⃣ 已存在的 ATA / PDA
    #[account(
        mut,
        associated_token::mint = mint_a,
        associated_token::authority = escrow,
    )]
    pub vault: InterfaceAccount<'info, TokenAccount>,

    // 2️⃣ init / init_if_needed 永远放后面
    #[account(
        init_if_needed,
        payer = maker,
        associated_token::mint = mint_a,
        associated_token::authority = maker,
    )]
    pub maker_ata_a: InterfaceAccount<'info, TokenAccount>,
}

七、经验总结(重点)

🔒 Anchor Accounts 顺序铁律

所有 init / init_if_needed 的账户

👉 必须写在所有“已存在账户”之后

尤其在以下场景中:

  • associated_token::*
  • InterfaceAccount
  • token_interface

🧠 心得

  • Anchor 的错误信息不一定指向真正的问题
  • 如果逻辑完全正确但报诡异错误:
    • 先检查 Accounts 顺序
  • 这是一个:
    • 文档很少提
    • 新手几乎必踩
    • 但进阶开发者一定要知道的坑

结语

这个问题本身不复杂,但定位它的过程非常消耗心智

一旦你理解了:

Anchor 的 Accounts 校验是“顺序 + 有副作用”的

之后再遇到类似问题,基本可以一眼看穿。

希望这篇总结,能帮你少踩一个大坑。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论