SC Day 78
Solana/Anchor 高级特性 — 零拷贝/剩余账户/自定义错误 + 增强 Escrow
### 1. #[zero_copy] 大账户优化
2026-06-27
第四阶段:综合实战 (73-80)solanaanchorzero-copyremaining-accountscustom-errorsescrowproduction
日期: 2026-06-27 方向: Solana / Anchor 阶段: 第四阶段:综合实战 (73-80) 标签: #solana #anchor #zero-copy #remaining-accounts #custom-errors #escrow #production
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | Anchor 高级特性:零拷贝、剩余账户、自定义错误码、事件日志 |
| 实操 | 增强 Escrow 程序:添加超时取消、自定义错误、事件记录 |
| 产出 | 生产级 Escrow 程序 + Anchor 高级模式速查表 |
核心概念
1. #[zero_copy] 大账户优化
在 Day 76 已经介绍了零拷贝的基本原理。这里深入介绍 Anchor 中的高级用法:
use anchor_lang::prelude::*;
/// 带有复杂嵌套结构的零拷贝账户
#[account(zero_copy)]
#[repr(C)]
pub struct GameState {
pub authority: Pubkey, // 32
pub game_id: u64, // 8
pub status: u8, // 1 (0=Pending, 1=Active, 2=Finished, 3=Cancelled)
pub player_count: u8, // 1
pub _padding: [u8; 6], // 6 (对齐到 8 字节边界)
pub players: [PlayerInfo; 8], // 8 * 80 = 640
pub scores: [u32; 8], // 8 * 4 = 32
pub created_at: i64, // 8
pub finished_at: i64, // 8
}
// 总计: 32 + 8 + 1 + 1 + 6 + 640 + 32 + 8 + 8 = 736 bytes
#[zero_copy]
#[repr(C)]
pub struct PlayerInfo {
pub pubkey: Pubkey, // 32
pub name: [u8; 32], // 32(固定长度字符串)
pub joined_at: i64, // 8
pub is_active: u8, // 1
pub _padding: [u8; 7], // 7
}
// 每个: 80 bytes
/// 零拷贝账户的初始化
#[derive(Accounts)]
pub struct CreateGame<'info> {
#[account(
init,
seeds = [b"game", authority.key().as_ref(), &game_id.to_le_bytes()],
bump,
payer = authority,
space = 8 + std::mem::size_of::<GameState>(),
)]
pub game: AccountLoader<'info, GameState>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
pub fn create_game(ctx: Context<CreateGame>, game_id: u64) -> Result<()> {
// load_init() 用于初始化零拷贝账户
let game = &mut ctx.accounts.game.load_init()?;
game.authority = ctx.accounts.authority.key();
game.game_id = game_id;
game.status = 0; // Pending
game.player_count = 0;
game.created_at = Clock::get()?.unix_timestamp;
game.finished_at = 0;
Ok(())
}
#[repr(C)] vs #[repr(packed)]
// #[repr(C)] - C 语言内存对齐规则
// 优点:跨语言兼容,可预测的布局
// 缺点:可能有填充字节(浪费空间)
#[zero_copy]
#[repr(C)]
pub struct AlignedStruct {
pub a: u8, // 1 byte
pub _pad1: [u8; 7], // 7 bytes padding (对齐到 8)
pub b: u64, // 8 bytes
pub c: u8, // 1 byte
pub _pad2: [u8; 7], // 7 bytes padding
}
// 总计: 24 bytes
// #[repr(packed)] - 无填充,紧凑布局
// 优点:节省空间
// 缺点:未对齐的访问可能更慢(某些平台会 panic)
// Anchor 推荐使用 #[repr(C)] + 显式 padding
2. remaining_accounts — 可变长度账户列表
当指令需要处理不确定数量的账户时,使用 remaining_accounts:
use anchor_lang::prelude::*;
#[program]
pub mod multi_transfer {
use super::*;
/// 批量转账:向多个接收者转账
/// remaining_accounts 中传入 [recipient1, recipient2, ...]
pub fn batch_airdrop(
ctx: Context<BatchAirdrop>,
amounts: Vec<u64>,
) -> Result<()> {
let recipients = &ctx.remaining_accounts;
require!(
recipients.len() == amounts.len(),
CustomError::MismatchedLengths
);
require!(recipients.len() <= 20, CustomError::TooManyRecipients);
let vault = &ctx.accounts.vault;
let authority_seeds = &[
b"vault".as_ref(),
vault.authority.as_ref(),
&[vault.bump],
];
let signer_seeds = &[&authority_seeds[..]];
for (i, recipient) in recipients.iter().enumerate() {
// 验证每个接收者账户
require!(
recipient.is_writable,
CustomError::AccountNotWritable
);
// 使用 CPI 转账
let transfer_ix = anchor_spl::token::Transfer {
from: ctx.accounts.vault_token.to_account_info(),
to: recipient.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
};
anchor_spl::token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
transfer_ix,
signer_seeds,
),
amounts[i],
)?;
}
emit!(AirdropCompleted {
recipient_count: recipients.len() as u32,
total_amount: amounts.iter().sum(),
});
Ok(())
}
/// 使用 remaining_accounts 实现动态多资产存款
pub fn multi_asset_deposit(ctx: Context<MultiDeposit>) -> Result<()> {
let remaining = &ctx.remaining_accounts;
// 每 3 个账户为一组: [user_token, vault_token, mint]
require!(remaining.len() % 3 == 0, CustomError::InvalidAccountCount);
let asset_count = remaining.len() / 3;
for i in 0..asset_count {
let user_token = &remaining[i * 3];
let vault_token = &remaining[i * 3 + 1];
let mint = &remaining[i * 3 + 2];
// 反序列化并验证
let user_token_account: Account<TokenAccount> =
Account::try_from(user_token)?;
let vault_token_account: Account<TokenAccount> =
Account::try_from(vault_token)?;
require!(
user_token_account.mint == vault_token_account.mint,
CustomError::MintMismatch
);
// 转账逻辑...
}
Ok(())
}
}
#[derive(Accounts)]
pub struct BatchAirdrop<'info> {
#[account(
seeds = [b"vault", vault.authority.as_ref()],
bump = vault.bump,
)]
pub vault: Account<'info, Vault>,
#[account(mut)]
pub vault_token: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
// remaining_accounts: 接收者 token 账户列表
}
3. 自定义错误码 (#[error_code])
/// 自定义错误 - 清晰的错误信息和唯一的错误码
#[error_code]
pub enum EscrowError {
/// 错误码从 6000 开始(Anchor 约定)
#[msg("Escrow has already been completed")]
AlreadyCompleted, // 6000
#[msg("Escrow has been cancelled")]
AlreadyCancelled, // 6001
#[msg("Escrow timeout has not been reached")]
TimeoutNotReached, // 6002
#[msg("Escrow has expired and can no longer be completed")]
EscrowExpired, // 6003
#[msg("Invalid escrow state for this operation")]
InvalidState, // 6004
#[msg("Deposit amount must be greater than zero")]
ZeroDeposit, // 6005
#[msg("Unauthorized: not the escrow maker or taker")]
Unauthorized, // 6006
#[msg("Amount exceeds escrow balance")]
InsufficientBalance, // 6007
}
// 使用自定义错误
pub fn complete_escrow(ctx: Context<CompleteEscrow>) -> Result<()> {
let escrow = &ctx.accounts.escrow;
// 使用 require! 宏 + 自定义错误
require!(escrow.status == EscrowStatus::Active as u8, EscrowError::InvalidState);
require!(
Clock::get()?.unix_timestamp <= escrow.expiry_time,
EscrowError::EscrowExpired
);
// 或使用 err! 宏
if ctx.accounts.taker.key() != escrow.taker {
return err!(EscrowError::Unauthorized);
}
Ok(())
}
// 客户端解析错误
// TypeScript:
// try {
// await program.methods.completeEscrow().rpc();
// } catch (err) {
// if (err instanceof anchor.AnchorError) {
// console.log("Error code:", err.error.errorCode.number); // 6003
// console.log("Error msg:", err.error.errorMessage); // "Escrow has expired..."
// }
// }
4. 事件日志 (emit!)
/// 事件定义
#[event]
pub struct EscrowCreated {
pub escrow_id: u64,
pub maker: Pubkey,
pub taker: Pubkey,
pub amount: u64,
pub token_mint: Pubkey,
pub expiry_time: i64,
pub timestamp: i64,
}
#[event]
pub struct EscrowCompleted {
pub escrow_id: u64,
pub maker: Pubkey,
pub taker: Pubkey,
pub amount: u64,
pub timestamp: i64,
}
#[event]
pub struct EscrowCancelled {
pub escrow_id: u64,
pub cancelled_by: Pubkey,
pub reason: String, // 注意:String 会增加序列化开销
pub timestamp: i64,
}
// 使用
pub fn create_escrow(ctx: Context<CreateEscrow>, amount: u64, expiry: i64) -> Result<()> {
// ... 业务逻辑 ...
emit!(EscrowCreated {
escrow_id: escrow.escrow_id,
maker: ctx.accounts.maker.key(),
taker: ctx.accounts.taker.key(),
amount,
token_mint: ctx.accounts.token_mint.key(),
expiry_time: expiry,
timestamp: Clock::get()?.unix_timestamp,
});
Ok(())
}
// TypeScript 监听事件:
// program.addEventListener("EscrowCreated", (event, slot) => {
// console.log("New escrow:", event.escrowId.toString());
// console.log("Amount:", event.amount.toString());
// });
代码实战
增强版 Escrow 程序
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer, Mint};
declare_id!("Escrw111111111111111111111111111111111111111");
#[program]
pub mod enhanced_escrow {
use super::*;
/// 创建 Escrow:maker 存入 token_a,等待 taker 用 token_b 交换
pub fn create(
ctx: Context<Create>,
escrow_id: u64,
deposit_amount: u64,
expected_amount: u64,
timeout_seconds: i64,
) -> Result<()> {
require!(deposit_amount > 0, EscrowError::ZeroDeposit);
require!(expected_amount > 0, EscrowError::ZeroDeposit);
require!(timeout_seconds > 0 && timeout_seconds <= 30 * 24 * 3600,
EscrowError::InvalidTimeout);
let now = Clock::get()?.unix_timestamp;
// 初始化 Escrow 账户
let escrow = &mut ctx.accounts.escrow;
escrow.escrow_id = escrow_id;
escrow.maker = ctx.accounts.maker.key();
escrow.taker = Pubkey::default(); // 任何人都可以接受
escrow.mint_a = ctx.accounts.mint_a.key();
escrow.mint_b = ctx.accounts.mint_b.key();
escrow.deposit_amount = deposit_amount;
escrow.expected_amount = expected_amount;
escrow.status = EscrowStatus::Active as u8;
escrow.created_at = now;
escrow.expiry_time = now + timeout_seconds;
escrow.bump = ctx.bumps.escrow;
escrow.vault_bump = ctx.bumps.vault;
// 将 token_a 从 maker 转到 vault
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.maker_token_a.to_account_info(),
to: ctx.accounts.vault.to_account_info(),
authority: ctx.accounts.maker.to_account_info(),
},
),
deposit_amount,
)?;
emit!(EscrowCreated {
escrow_id,
maker: ctx.accounts.maker.key(),
mint_a: ctx.accounts.mint_a.key(),
mint_b: ctx.accounts.mint_b.key(),
deposit_amount,
expected_amount,
expiry_time: escrow.expiry_time,
timestamp: now,
});
Ok(())
}
/// 完成交换:taker 发送 token_b 给 maker,获得 vault 中的 token_a
pub fn exchange(ctx: Context<Exchange>) -> Result<()> {
let escrow = &ctx.accounts.escrow;
// 状态检查
require!(escrow.status == EscrowStatus::Active as u8, EscrowError::InvalidState);
// 超时检查
let now = Clock::get()?.unix_timestamp;
require!(now <= escrow.expiry_time, EscrowError::EscrowExpired);
// 步骤 1:taker 发送 token_b 给 maker
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.taker_token_b.to_account_info(),
to: ctx.accounts.maker_token_b.to_account_info(),
authority: ctx.accounts.taker.to_account_info(),
},
),
escrow.expected_amount,
)?;
// 步骤 2:vault 中的 token_a 发送给 taker(PDA 签名)
let maker_key = escrow.maker;
let escrow_id_bytes = escrow.escrow_id.to_le_bytes();
let seeds = &[
b"escrow".as_ref(),
maker_key.as_ref(),
escrow_id_bytes.as_ref(),
&[escrow.bump],
];
let signer_seeds = &[&seeds[..]];
let vault_seeds = &[
b"vault".as_ref(),
ctx.accounts.escrow.to_account_info().key.as_ref(),
&[escrow.vault_bump],
];
let vault_signer = &[&vault_seeds[..]];
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.taker_token_a.to_account_info(),
authority: ctx.accounts.vault_authority.to_account_info(),
},
vault_signer,
),
escrow.deposit_amount,
)?;
// 更新状态
let escrow = &mut ctx.accounts.escrow;
escrow.status = EscrowStatus::Completed as u8;
emit!(EscrowCompleted {
escrow_id: escrow.escrow_id,
maker: escrow.maker,
taker: ctx.accounts.taker.key(),
deposit_amount: escrow.deposit_amount,
expected_amount: escrow.expected_amount,
timestamp: now,
});
Ok(())
}
/// 超时取消:maker 在超时后可以取回存款
pub fn cancel_timeout(ctx: Context<CancelTimeout>) -> Result<()> {
let escrow = &ctx.accounts.escrow;
require!(escrow.status == EscrowStatus::Active as u8, EscrowError::InvalidState);
let now = Clock::get()?.unix_timestamp;
require!(now > escrow.expiry_time, EscrowError::TimeoutNotReached);
// 从 vault 返还 token_a 给 maker
let vault_seeds = &[
b"vault".as_ref(),
ctx.accounts.escrow.to_account_info().key.as_ref(),
&[escrow.vault_bump],
];
let vault_signer = &[&vault_seeds[..]];
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.maker_token_a.to_account_info(),
authority: ctx.accounts.vault_authority.to_account_info(),
},
vault_signer,
),
escrow.deposit_amount,
)?;
// 更新状态
let escrow = &mut ctx.accounts.escrow;
escrow.status = EscrowStatus::Cancelled as u8;
emit!(EscrowCancelled {
escrow_id: escrow.escrow_id,
cancelled_by: ctx.accounts.maker.key(),
timestamp: now,
});
Ok(())
}
/// Maker 主动取消(超时前也可以,但可能需要额外条件)
pub fn cancel_by_maker(ctx: Context<CancelByMaker>) -> Result<()> {
let escrow = &ctx.accounts.escrow;
require!(escrow.status == EscrowStatus::Active as u8, EscrowError::InvalidState);
// 返还资金(与 cancel_timeout 类似)
let vault_seeds = &[
b"vault".as_ref(),
ctx.accounts.escrow.to_account_info().key.as_ref(),
&[escrow.vault_bump],
];
let vault_signer = &[&vault_seeds[..]];
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.maker_token_a.to_account_info(),
authority: ctx.accounts.vault_authority.to_account_info(),
},
vault_signer,
),
escrow.deposit_amount,
)?;
let escrow = &mut ctx.accounts.escrow;
escrow.status = EscrowStatus::Cancelled as u8;
emit!(EscrowCancelled {
escrow_id: escrow.escrow_id,
cancelled_by: ctx.accounts.maker.key(),
timestamp: Clock::get()?.unix_timestamp,
});
Ok(())
}
}
// ============ 账户定义 ============
#[derive(Accounts)]
#[instruction(escrow_id: u64)]
pub struct Create<'info> {
#[account(
init,
seeds = [b"escrow", maker.key().as_ref(), &escrow_id.to_le_bytes()],
bump,
payer = maker,
space = 8 + EscrowState::INIT_SPACE,
)]
pub escrow: Account<'info, EscrowState>,
#[account(
init,
seeds = [b"vault", escrow.key().as_ref()],
bump,
payer = maker,
token::mint = mint_a,
token::authority = vault_authority,
)]
pub vault: Account<'info, TokenAccount>,
/// CHECK: PDA 作为 vault 的 authority
#[account(
seeds = [b"vault", escrow.key().as_ref()],
bump,
)]
pub vault_authority: AccountInfo<'info>,
pub mint_a: Account<'info, Mint>,
pub mint_b: Account<'info, Mint>,
#[account(
mut,
constraint = maker_token_a.mint == mint_a.key(),
constraint = maker_token_a.owner == maker.key(),
)]
pub maker_token_a: Account<'info, TokenAccount>,
#[account(mut)]
pub maker: Signer<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct Exchange<'info> {
#[account(
mut,
seeds = [b"escrow", escrow.maker.as_ref(), &escrow.escrow_id.to_le_bytes()],
bump = escrow.bump,
)]
pub escrow: Account<'info, EscrowState>,
#[account(
mut,
seeds = [b"vault", escrow.key().as_ref()],
bump = escrow.vault_bump,
)]
pub vault: Account<'info, TokenAccount>,
/// CHECK: vault authority PDA
#[account(
seeds = [b"vault", escrow.key().as_ref()],
bump = escrow.vault_bump,
)]
pub vault_authority: AccountInfo<'info>,
#[account(
mut,
constraint = taker_token_a.mint == escrow.mint_a,
)]
pub taker_token_a: Account<'info, TokenAccount>,
#[account(
mut,
constraint = taker_token_b.mint == escrow.mint_b,
constraint = taker_token_b.owner == taker.key(),
)]
pub taker_token_b: Account<'info, TokenAccount>,
#[account(
mut,
constraint = maker_token_b.mint == escrow.mint_b,
constraint = maker_token_b.owner == escrow.maker,
)]
pub maker_token_b: Account<'info, TokenAccount>,
#[account(mut)]
pub taker: Signer<'info>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct CancelTimeout<'info> {
#[account(
mut,
seeds = [b"escrow", escrow.maker.as_ref(), &escrow.escrow_id.to_le_bytes()],
bump = escrow.bump,
has_one = maker,
)]
pub escrow: Account<'info, EscrowState>,
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
/// CHECK: vault authority PDA
pub vault_authority: AccountInfo<'info>,
#[account(
mut,
constraint = maker_token_a.mint == escrow.mint_a,
)]
pub maker_token_a: Account<'info, TokenAccount>,
pub maker: Signer<'info>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct CancelByMaker<'info> {
#[account(
mut,
has_one = maker,
)]
pub escrow: Account<'info, EscrowState>,
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
/// CHECK: vault authority PDA
pub vault_authority: AccountInfo<'info>,
#[account(mut)]
pub maker_token_a: Account<'info, TokenAccount>,
pub maker: Signer<'info>,
pub token_program: Program<'info, Token>,
}
// ============ 数据模型 ============
#[account]
#[derive(InitSpace)]
pub struct EscrowState {
pub escrow_id: u64,
pub maker: Pubkey,
pub taker: Pubkey,
pub mint_a: Pubkey,
pub mint_b: Pubkey,
pub deposit_amount: u64,
pub expected_amount: u64,
pub status: u8,
pub created_at: i64,
pub expiry_time: i64,
pub bump: u8,
pub vault_bump: u8,
}
#[repr(u8)]
pub enum EscrowStatus {
Active = 0,
Completed = 1,
Cancelled = 2,
}
// ============ 错误和事件 ============
#[error_code]
pub enum EscrowError {
#[msg("Deposit amount must be greater than zero")]
ZeroDeposit,
#[msg("Invalid escrow state for this operation")]
InvalidState,
#[msg("Escrow has expired")]
EscrowExpired,
#[msg("Timeout has not been reached yet")]
TimeoutNotReached,
#[msg("Invalid timeout duration")]
InvalidTimeout,
#[msg("Unauthorized")]
Unauthorized,
}
#[event]
pub struct EscrowCreated {
pub escrow_id: u64,
pub maker: Pubkey,
pub mint_a: Pubkey,
pub mint_b: Pubkey,
pub deposit_amount: u64,
pub expected_amount: u64,
pub expiry_time: i64,
pub timestamp: i64,
}
#[event]
pub struct EscrowCompleted {
pub escrow_id: u64,
pub maker: Pubkey,
pub taker: Pubkey,
pub deposit_amount: u64,
pub expected_amount: u64,
pub timestamp: i64,
}
#[event]
pub struct EscrowCancelled {
pub escrow_id: u64,
pub cancelled_by: Pubkey,
pub timestamp: i64,
}
关键要点总结
| 特性 | 用途 | 注意事项 |
|---|---|---|
| #[zero_copy] | 大账户高效读写 | 字段必须固定大小,需要 #[repr(C)] |
| remaining_accounts | 动态长度账户列表 | 需要手动验证每个账户 |
| #[error_code] | 清晰的错误反馈 | 错误码从 6000 开始 |
| emit! | 链上事件日志 | 可被客户端监听和索引 |
| init_if_needed | 条件创建账户 | 需要谨慎使用,可能有安全风险 |
| has_one | 字段匹配验证 | 编译时生成约束检查代码 |
常见误区
- "remaining_accounts 自动验证" — 错误!remaining_accounts 没有任何自动验证,必须手动检查
- "emit! 和 msg! 等价" — 不对。emit! 产生结构化的事件日志(可索引),msg! 只产生文本日志
- "错误码是任意的" — Anchor 错误码从 6000 开始,系统错误和 Anchor 内部错误占用了更低的范围
- "超时就能自动取消 Escrow" — Solana 没有自动执行机制,超时只是允许 maker 手动取消
- "init_if_needed 没有风险" — 有!如果账户已被其他程序创建(但 owner 不对),可能导致意外行为
面试关联
面试题:Solana Escrow 设计中有哪些关键的安全考虑?
30 秒回答: 核心考虑包括:PDA 签名确保只有程序能控制 vault、超时机制防止资金永久锁定、状态机确保操作原子性、所有传入账户的身份和所有权验证。
2 分钟回答:
Escrow 设计的安全性体现在四个层面。第一是资金安全——使用 PDA 控制的 vault 账户存放资金,确保只有程序逻辑能转移资金,没有单一私钥可以直接提取。第二是超时机制——没有超时的 Escrow 意味着资金可能永久锁定,设计合理的超时窗口和取消流程至关重要。第三是状态管理——Escrow 有明确的状态机(Active/Completed/Cancelled),每个操作都检查当前状态,防止重复完成或取消。第四是账户验证——Anchor 的 has_one 和 constraint 确保传入的 token 账户、mint 地址都匹配,防止攻击者传入伪造账户窃取资金。
参考资源
| 资源 | 说明 |
|---|---|
| Anchor Book | Anchor 官方教程 |
| Anchor Examples | 官方示例代码 |
| Solana Cookbook - PDAs | PDA 使用指南 |
| Token Swap Example | Anchor Escrow 测试 |
| Anchor Error Handling | 错误处理文档 |