返回 SC 笔记
SC Day 35

Solana/Anchor - 框架入门 - #[program]/#[derive(Accounts)]/#[account] + Hello World

### 一、为什么需要 Anchor?

2026-05-05
第二阶段:框架实战
SolanaAnchorFrameworkHelloWorldRust

日期: 2026-05-05 方向: Rust/Solana 阶段: 第二阶段:框架实战 标签: #Solana #Anchor #Framework #HelloWorld #Rust


今日目标

  1. 安装 Anchor 开发框架并理解其设计理念
  2. 掌握 Anchor 项目结构(programs/tests/migrations)
  3. 深入理解三个核心宏:#[program]#[derive(Accounts)]#[account]
  4. 实现一个完整的 Hello World Anchor 程序(initialize + greet)并编写 TypeScript 测试
  5. 对比 Anchor 与原生 Solana 开发的差异

核心概念

一、为什么需要 Anchor?

原生 Solana 程序开发非常繁琐和容易出错:

// 原生 Solana: 手动序列化/反序列化 + 手动账户验证
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    // 1. 手动解析 instruction_data (哪个指令?什么参数?)
    let instruction = MyInstruction::try_from_slice(instruction_data)?;

    // 2. 手动获取和验证每个账户
    let account_iter = &mut accounts.iter();
    let user = next_account_info(account_iter)?;
    let data_account = next_account_info(account_iter)?;
    let system_program = next_account_info(account_iter)?;

    // 3. 手动验证签名
    if !user.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // 4. 手动验证 owner
    if data_account.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }

    // 5. 手动序列化/反序列化数据
    let mut data = MyData::try_from_slice(&data_account.data.borrow())?;
    data.value += 1;
    data.serialize(&mut *data_account.data.borrow_mut())?;

    Ok(())
}

Anchor 通过 Rust 宏自动处理上述所有样板代码:

// Anchor: 声明式,安全,简洁
#[program]
pub mod my_program {
    use super::*;

    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        ctx.accounts.counter.value += 1;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
}

#[account]
pub struct Counter {
    pub value: u64,
}

二、Anchor 安装与项目初始化

安装 Anchor

# 方式 1: 使用 avm (Anchor Version Manager,推荐)
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install latest
avm use latest

# 验证安装
anchor --version
# anchor-cli 0.30.1

# 方式 2: 使用 cargo 直接安装
cargo install --git https://github.com/coral-xyz/anchor --tag v0.30.1 anchor-cli

创建项目

# 初始化新项目
anchor init hello-anchor
cd hello-anchor

# 项目结构:
hello-anchor/
├── Anchor.toml          # Anchor 配置文件(类似 Foundry 的 foundry.toml)
├── Cargo.toml           # Rust workspace 配置
├── app/                 # 前端应用(可选)
├── migrations/          # 部署脚本
│   └── deploy.ts
├── programs/            # Solana 程序源码
│   └── hello-anchor/
│       ├── Cargo.toml
│       └── src/
│           └── lib.rs   # 主程序文件
├── tests/               # TypeScript 测试
│   └── hello-anchor.ts
└── tsconfig.json

Anchor.toml 配置

[features]
seeds = false
skip-lint = false

[programs.localnet]
hello_anchor = "你的程序ID"

[programs.devnet]
hello_anchor = "你的程序ID"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "Localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

三、Anchor 三大核心宏

3.1 #[program] — 程序定义

#[program] 宏标记一个模块为 Solana 程序的入口。模块中的每个 public 函数都会成为一个可调用的指令(instruction)。

use anchor_lang::prelude::*;

declare_id!("你的程序ID"); // 程序的链上地址

#[program]
pub mod hello_anchor {
    use super::*;

    // 每个函数 = 一个 Instruction
    // 第一个参数必须是 Context<T>
    // 返回类型必须是 Result<()>
    pub fn initialize(ctx: Context<Initialize>, name: String) -> Result<()> {
        let greeting = &mut ctx.accounts.greeting;
        greeting.name = name;
        greeting.count = 0;
        greeting.authority = ctx.accounts.user.key();
        msg!("Greeting account initialized for: {}", greeting.name);
        Ok(())
    }

    pub fn greet(ctx: Context<Greet>) -> Result<()> {
        let greeting = &mut ctx.accounts.greeting;
        greeting.count += 1;
        msg!("Hello {}! You've been greeted {} times.",
            greeting.name, greeting.count);
        Ok(())
    }
}

#[program] 宏自动生成的内容

1. process_instruction 入口函数
2. Instruction 路由(根据函数名的 hash 分发)
3. 参数的 Borsh 反序列化
4. 账户的验证和加载
5. 错误处理和转换

3.2 #[derive(Accounts)] — 账户验证

#[derive(Accounts)] 宏定义一个指令需要的所有账户,以及每个账户的约束条件。这是 Anchor 安全性的核心。

#[derive(Accounts)]
pub struct Initialize<'info> {
    // #[account(init, ...)] — 创建新账户
    #[account(
        init,                               // 创建新账户
        payer = user,                       // 谁支付 rent
        space = 8 + Greeting::INIT_SPACE,   // 账户大小(8 字节 discriminator + 数据)
    )]
    pub greeting: Account<'info, Greeting>,

    // #[account(mut)] — 标记为可写(会修改 lamports)
    #[account(mut)]
    pub user: Signer<'info>,  // Signer 类型自动验证签名

    // System Program 用于创建账户
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Greet<'info> {
    // 已存在的账户,标记为可写(会修改数据)
    #[account(mut)]
    pub greeting: Account<'info, Greeting>,
}

常用账户约束

约束说明示例
init创建新账户#[account(init, payer = user, space = 100)]
mut可写账户#[account(mut)]
has_one验证字段匹配#[account(has_one = authority)]
constraint自定义约束#[account(constraint = amount > 0)]
seedsPDA 种子#[account(seeds = [b"vault"], bump)]
close关闭账户退还 rent#[account(mut, close = user)]
realloc调整账户大小#[account(realloc = new_size, ...)]

Anchor 账户类型

类型说明自动验证
Account<'info, T>拥有数据 T 的账户owner、discriminator
Signer<'info>必须签名的账户is_signer
Program<'info, T>程序账户executable、program_id
SystemAccount<'info>系统账户owner = System Program
UncheckedAccount<'info>不做检查的账户无(需手动验证)

3.3 #[account] — 数据结构定义

#[account] 宏定义程序的数据结构(存储在 Data Account 中)。

#[account]
#[derive(InitSpace)]  // 自动计算 space
pub struct Greeting {
    #[max_len(50)]     // String 需要指定最大长度
    pub name: String,
    pub count: u64,
    pub authority: Pubkey,
}

#[account] 宏自动做的事

1. 添加 8 字节 discriminator(前缀,用于识别账户类型)
   discriminator = sha256("account:<AccountName>")[..8]

2. 实现 Borsh 序列化/反序列化 (BorshSerialize + BorshDeserialize)

3. 实现 AccountSerialize + AccountDeserialize trait

4. 实现 Owner trait(绑定到当前程序)

Space 计算规则

总 space = 8 (discriminator) + 数据大小

基本类型:
  bool     → 1 byte
  u8/i8    → 1 byte
  u16/i16  → 2 bytes
  u32/i32  → 4 bytes
  u64/i64  → 8 bytes
  u128/i128 → 16 bytes
  Pubkey   → 32 bytes

复合类型:
  String   → 4 (length prefix) + content bytes
  Vec<T>   → 4 (length prefix) + n * size_of::<T>()
  Option<T> → 1 + size_of::<T>()

示例: Greeting
  name (max 50 chars) → 4 + 50 = 54 bytes
  count (u64)         → 8 bytes
  authority (Pubkey)  → 32 bytes
  total data          → 94 bytes
  total space         → 8 + 94 = 102 bytes

四、Anchor 开发工作流

1. anchor build     → 编译程序(生成 IDL 和 TypeScript 类型)
2. anchor test      → 运行测试(自动启动 localnet)
3. anchor deploy    → 部署到指定网络
4. anchor verify    → 验证链上代码与本地一致

代码实战

完整的 Hello World 程序

程序代码

// programs/hello-anchor/src/lib.rs
use anchor_lang::prelude::*;

declare_id!("HeLLo11111111111111111111111111111111111111"); // 临时 ID,部署时会更新

#[program]
pub mod hello_anchor {
    use super::*;

    /// 初始化一个 Greeting 账户
    pub fn initialize(ctx: Context<Initialize>, name: String) -> Result<()> {
        require!(name.len() <= 50, HelloError::NameTooLong);

        let greeting = &mut ctx.accounts.greeting;
        greeting.name = name.clone();
        greeting.count = 0;
        greeting.authority = ctx.accounts.user.key();
        greeting.bump = ctx.bumps.greeting;

        msg!("Greeting account initialized!");
        msg!("Name: {}", name);
        msg!("Authority: {}", greeting.authority);

        Ok(())
    }

    /// 问候并递增计数器
    pub fn greet(ctx: Context<Greet>) -> Result<()> {
        let greeting = &mut ctx.accounts.greeting;
        greeting.count = greeting.count.checked_add(1)
            .ok_or(HelloError::Overflow)?;

        msg!("Hello, {}! Greeted {} times.", greeting.name, greeting.count);

        Ok(())
    }

    /// 更新名字(只有 authority 可以操作)
    pub fn update_name(ctx: Context<UpdateName>, new_name: String) -> Result<()> {
        require!(new_name.len() <= 50, HelloError::NameTooLong);

        let greeting = &mut ctx.accounts.greeting;
        let old_name = greeting.name.clone();
        greeting.name = new_name.clone();

        msg!("Name updated: {} -> {}", old_name, new_name);

        Ok(())
    }

    /// 关闭账户(退还 rent 给 authority)
    pub fn close_greeting(_ctx: Context<CloseGreeting>) -> Result<()> {
        msg!("Greeting account closed!");
        Ok(())
    }
}

// ===== Accounts Structs =====

#[derive(Accounts)]
#[instruction(name: String)]  // 允许在约束中使用指令参数
pub struct Initialize<'info> {
    #[account(
        init,
        payer = user,
        space = 8 + Greeting::INIT_SPACE,
        seeds = [b"greeting", user.key().as_ref()],  // PDA
        bump,
    )]
    pub greeting: Account<'info, Greeting>,

    #[account(mut)]
    pub user: Signer<'info>,

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Greet<'info> {
    #[account(mut)]
    pub greeting: Account<'info, Greeting>,
}

#[derive(Accounts)]
pub struct UpdateName<'info> {
    #[account(
        mut,
        has_one = authority,  // 验证 greeting.authority == authority.key()
    )]
    pub greeting: Account<'info, Greeting>,

    pub authority: Signer<'info>,
}

#[derive(Accounts)]
pub struct CloseGreeting<'info> {
    #[account(
        mut,
        has_one = authority,
        close = authority,  // 关闭账户,rent 退还给 authority
    )]
    pub greeting: Account<'info, Greeting>,

    #[account(mut)]
    pub authority: Signer<'info>,
}

// ===== Data Account =====

#[account]
#[derive(InitSpace)]
pub struct Greeting {
    #[max_len(50)]
    pub name: String,     // 4 + 50 = 54 bytes
    pub count: u64,       // 8 bytes
    pub authority: Pubkey, // 32 bytes
    pub bump: u8,         // 1 byte
}
// Total: 8 (discriminator) + 54 + 8 + 32 + 1 = 103 bytes

// ===== Custom Errors =====

#[error_code]
pub enum HelloError {
    #[msg("Name is too long (max 50 characters)")]
    NameTooLong,
    #[msg("Counter overflow")]
    Overflow,
}

TypeScript 测试

// tests/hello-anchor.ts
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { HelloAnchor } from "../target/types/hello_anchor";
import { expect } from "chai";

describe("hello-anchor", () => {
  // 配置 provider(连接到 localnet)
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>;
  const user = provider.wallet;

  // 派生 PDA 地址
  const [greetingPDA, bump] = anchor.web3.PublicKey.findProgramAddressSync(
    [Buffer.from("greeting"), user.publicKey.toBuffer()],
    program.programId
  );

  it("Initializes a greeting account", async () => {
    const tx = await program.methods
      .initialize("Alice")
      .accounts({
        greeting: greetingPDA,
        user: user.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .rpc();

    console.log("Initialize tx:", tx);

    // 获取账户数据
    const greetingAccount = await program.account.greeting.fetch(greetingPDA);

    expect(greetingAccount.name).to.equal("Alice");
    expect(greetingAccount.count.toNumber()).to.equal(0);
    expect(greetingAccount.authority.toString()).to.equal(
      user.publicKey.toString()
    );
    console.log("Greeting account:", greetingAccount);
  });

  it("Greets and increments counter", async () => {
    // 第一次问候
    await program.methods
      .greet()
      .accounts({
        greeting: greetingPDA,
      })
      .rpc();

    let greetingAccount = await program.account.greeting.fetch(greetingPDA);
    expect(greetingAccount.count.toNumber()).to.equal(1);

    // 第二次问候
    await program.methods
      .greet()
      .accounts({
        greeting: greetingPDA,
      })
      .rpc();

    greetingAccount = await program.account.greeting.fetch(greetingPDA);
    expect(greetingAccount.count.toNumber()).to.equal(2);
  });

  it("Updates name (only authority)", async () => {
    await program.methods
      .updateName("Bob")
      .accounts({
        greeting: greetingPDA,
        authority: user.publicKey,
      })
      .rpc();

    const greetingAccount = await program.account.greeting.fetch(greetingPDA);
    expect(greetingAccount.name).to.equal("Bob");
  });

  it("Fails to update name with wrong authority", async () => {
    // 创建一个不同的密钥对
    const wrongUser = anchor.web3.Keypair.generate();

    try {
      await program.methods
        .updateName("Hacker")
        .accounts({
          greeting: greetingPDA,
          authority: wrongUser.publicKey,
        })
        .signers([wrongUser])
        .rpc();

      // 不应该执行到这里
      expect.fail("Should have thrown an error");
    } catch (err) {
      // 预期会失败(has_one 约束验证)
      expect(err).to.be.instanceOf(Error);
      console.log("Error (expected):", err.message.substring(0, 100));
    }
  });

  it("Closes greeting account", async () => {
    // 记录关闭前的余额
    const balanceBefore = await provider.connection.getBalance(user.publicKey);

    await program.methods
      .closeGreeting()
      .accounts({
        greeting: greetingPDA,
        authority: user.publicKey,
      })
      .rpc();

    // 验证账户已被关闭
    const greetingAccount = await provider.connection.getAccountInfo(
      greetingPDA
    );
    expect(greetingAccount).to.be.null;

    // 验证 rent 已退还
    const balanceAfter = await provider.connection.getBalance(user.publicKey);
    expect(balanceAfter).to.be.greaterThan(balanceBefore);
    console.log("Rent refunded:", (balanceAfter - balanceBefore) / 1e9, "SOL");
  });
});

运行测试

# 构建程序
anchor build

# 运行测试(自动启动本地验证器)
anchor test

# 如果已经有运行中的本地验证器
anchor test --skip-local-validator

# 部署到 devnet
anchor deploy --provider.cluster devnet

五、Anchor vs 原生 Solana 开发对比

维度原生 SolanaAnchor
代码量300+ 行100 行
账户验证手动逐一检查声明式约束
序列化手动 Borsh自动
错误处理ProgramError自定义 error_code
IDL 生成自动生成(类似 ABI)
TypeScript 类型手动写从 IDL 自动生成
学习曲线陡峭相对平缓
灵活性完全控制有宏的约束
Gas 效率更优(手动优化)略高开销
生态使用率少数高性能项目主流选择

Anchor 生成的 IDL 示例(类似 Solidity 的 ABI):

{
  "version": "0.1.0",
  "name": "hello_anchor",
  "instructions": [
    {
      "name": "initialize",
      "accounts": [
        { "name": "greeting", "isMut": true, "isSigner": false },
        { "name": "user", "isMut": true, "isSigner": true },
        { "name": "systemProgram", "isMut": false, "isSigner": false }
      ],
      "args": [
        { "name": "name", "type": "string" }
      ]
    }
  ],
  "accounts": [
    {
      "name": "Greeting",
      "type": {
        "kind": "struct",
        "fields": [
          { "name": "name", "type": "string" },
          { "name": "count", "type": "u64" },
          { "name": "authority", "type": "publicKey" },
          { "name": "bump", "type": "u8" }
        ]
      }
    }
  ]
}

关键要点总结

Anchor 三大核心宏

#[program]           → 定义程序入口和所有指令
                        每个 pub fn = 一个 Instruction
                        参数自动反序列化

#[derive(Accounts)]  → 定义每个指令需要的账户和约束
                        init/mut/has_one/seeds/close
                        自动验证签名、owner、discriminator

#[account]           → 定义链上数据结构
                        自动添加 8 字节 discriminator
                        自动实现 Borsh 序列化
                        自动绑定 owner

开发心智模型

设计程序时的思考顺序:

1. 定义数据结构 (#[account])
   → 需要存储什么数据?
   → 计算 space

2. 定义指令 (#[program])
   → 用户可以做哪些操作?
   → 每个操作修改什么数据?

3. 定义账户约束 (#[derive(Accounts)])
   → 每个指令需要哪些账户?
   → 谁需要签名?谁需要付钱?
   → 有什么安全约束?

常见误区

  1. 误区:Anchor 和 Hardhat 一样是测试框架

    • 事实:Anchor 是一个完整的开发框架,包括程序编写、编译、测试、部署的全套工具
  2. 误区:#[account(init)] 创建的账户可以重复创建

    • 事实:init 约束检查账户是否已存在。如果已初始化,会报错
  3. 误区:Space 计算不需要加 8

    • 事实:每个 Anchor 账户都有 8 字节 discriminator 前缀,必须加上
  4. 误区:任何人都可以修改 Account 的数据

    • 事实:只有 Account 的 owner 程序才能修改。Anchor 通过 #[account] 宏自动设置 owner
  5. 误区:declare_id! 中的地址不重要

    • 事实:它必须匹配链上部署的程序地址,否则 Anchor 的安全检查会失败

面试关联

Q1: Anchor 框架的核心价值是什么?

简短回答:Anchor 通过 Rust 宏自动处理账户验证、序列化和安全检查,让开发者用声明式代码取代大量手动样板代码,同时生成 IDL 方便客户端集成。

详细回答

  • 安全性#[derive(Accounts)] 的约束系统自动验证账户 owner、signer、可写性等,避免了原生开发中常见的安全遗漏
  • 开发效率:减少约 70% 的样板代码
  • 类型安全:从 IDL 自动生成 TypeScript 类型,前后端类型一致
  • 标准化:统一的项目结构和开发流程,降低团队协作成本

Q2: 解释 Anchor 中 PDA 的创建流程

答案

  1. #[derive(Accounts)] 中使用 seedsbump 约束声明 PDA
  2. init + seeds = Anchor 自动调用 create_account + 设置 PDA
  3. PDA 地址由 seeds + program_id + bump 确定性生成
  4. bump 会自动存储在 ctx.bumps 中,建议保存到账户数据里供后续使用

Q3: Anchor 的 discriminator 是什么?有什么作用?

答案

  • Discriminator 是每个 Anchor 账户数据的前 8 字节
  • 计算方式:sha256("account:<AccountStructName>")[..8]
  • 作用:当程序读取账户数据时,首先验证 discriminator 是否匹配,防止传入错误类型的账户
  • 这是 Anchor 类型安全的关键保障

参考资源

  1. Anchor 官方文档 — 框架文档
  2. Anchor Book — 深度教程
  3. Anchor GitHub — 源码和示例
  4. Anchor by Example — 代码示例集
  5. Solana Cookbook - Anchor — 实用参考
  6. Solana Playground — 浏览器内 Anchor 开发
  7. Coral Anchor Examples — 官方测试用例