深入理解账户系统
Solana 账户模型回顾
在 Solana 上,所有状态都存储在账户中。每个账户都遵循相同的基础结构:
rustpub struct Account { pub lamports: u64, // 账户余额(以 lamports 计) pub data: Vec<u8>, // 账户数据(字节数组) pub owner: Pubkey, // 拥有该账户的程序 pub executable: bool, // 是否为可执行程序 pub rent_epoch: Epoch, // 租金相关(已弃用) }
账户的关键特性:
- 所有权:只有账户的 owner 程序才能修改其 data 字段
- 数据:存储为原始字节,需要程序自行解释
- 租金:账户需要维持最低余额以避免被回收
程序账户与鉴别器
当你使用 #[account] 宏定义数据结构时,Anchor 会自动添加一个 8 字节的鉴别器(Discriminator)作为前缀:
rust#[account] #[derive(InitSpace)] pub struct Counter { pub count: u64, // 8 字节 pub authority: Pubkey, // 32 字节 } // 实际存储:8(鉴别器) + 8(count) + 32(authority) = 48 字节
鉴别器使用 sha256("account:<StructName>")[0..8] 计算,确保:
- 账户类型可被唯一识别
- 防止将错误类型的账户传入指令
- 检测未初始化的账户
从 Anchor v0.31.0 开始,你还可以指定自定义鉴别器:
rust#[account(discriminator = [1, 2, 3, 4, 5, 6, 7, 8])] pub struct CustomAccount { pub data: u64, }
账户类型详解
Anchor 提供了多种账户类型,每种都有特定的用途和验证逻辑:
Signer - 签名者账户
验证账户已签署当前交易:
rust#[derive(Accounts)] pub struct Transfer<'info> { #[account(mut)] pub sender: Signer<'info>, // 必须签署交易 // ... }
Account - 程序数据账户
反序列化并验证程序拥有的数据账户:
rust#[derive(Accounts)] pub struct UpdateCounter<'info> { #[account(mut)] pub counter: Account<'info, Counter>, // 自动反序列化 // ... }
SystemAccount - 系统账户
由系统程序拥有的普通账户,通常用作 SOL 转账目标:
rust#[derive(Accounts)] pub struct ReceivePayment<'info> { #[account(mut)] pub recipient: SystemAccount<'info>, // ... }
Program - 程序账户
验证账户是可执行程序且地址匹配:
rust#[derive(Accounts)] pub struct CallProgram<'info> { pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, // ... }
UncheckedAccount - 未验证账户
跳过所有自动验证,需要手动实现安全检查:
rust#[derive(Accounts)] pub struct CustomValidation<'info> { /// CHECK: 在指令逻辑中手动验证 pub unchecked: UncheckedAccount<'info>, // ... }
使用 UncheckedAccount 时必须添加 /// CHECK: 注释说明验证逻辑。
Option - 可选账户
允许账户在交易中省略:
rust#[derive(Accounts)] pub struct OptionalTransfer<'info> { pub optional_recipient: Option<Account<'info, TokenAccount>>, // ... }
当设为 None 时,Anchor 使用程序 ID 作为占位地址。
Box - 堆分配账户
将账户数据分配到堆上,减少栈内存使用:
rust#[derive(Accounts)] pub struct LargeData<'info> { pub large_account: Box<Account<'info, LargeDataType>>, // ... }
账户约束详解
约束通过 #[account(...)] 属性指定,在指令执行前自动验证。
初始化约束
创建新账户:
rust#[account( init, payer = authority, // 支付租金的账户 space = 8 + Counter::INIT_SPACE // 分配空间 )] pub counter: Account<'info, Counter>,
使用 PDA 初始化:
rust#[account( init, payer = authority, space = 8 + Vault::INIT_SPACE, seeds = [b"vault", authority.key().as_ref()], bump )] pub vault: Account<'info, Vault>,
可变性约束
标记账户可被修改:
rust#[account(mut)] pub counter: Account<'info, Counter>,
关联验证约束
验证账户字段与另一账户匹配:
rust#[account( mut, has_one = authority @ CustomError::Unauthorized )] pub counter: Account<'info, Counter>,
地址约束
验证账户地址匹配特定值:
rust#[account(address = ADMIN_PUBKEY)] pub admin: Signer<'info>,
所有者约束
验证账户由特定程序拥有:
rust#[account(owner = token::ID)] pub token_account: AccountInfo<'info>,
PDA 验证约束
验证账户是正确的程序派生地址:
rust#[account( seeds = [b"vault", authority.key().as_ref()], bump = vault.bump )] pub vault: Account<'info, Vault>,
自定义约束
使用任意布尔表达式验证:
rust#[account( constraint = counter.count < 100 @ CustomError::CounterFull )] pub counter: Account<'info, Counter>,
PDA(程序派生地址)
PDA 是由程序 ID 和一组种子确定性派生的地址,不在 ed25519 曲线上,因此没有对应的私钥。
PDA 的核心特性
- 确定性:相同的种子 + 程序 ID 总是生成相同的地址
- 程序签名:PDA 可以代表程序签署 CPI
- 无私钥:只有程序能控制 PDA
创建和使用 PDA
rust#[derive(Accounts)] pub struct CreateVault<'info> { #[account( init, payer = user, space = 8 + Vault::INIT_SPACE, seeds = [b"vault", user.key().as_ref()], bump )] pub vault: Account<'info, Vault>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] #[derive(InitSpace)] pub struct Vault { pub authority: Pubkey, pub balance: u64, pub bump: u8, // 存储 bump 以节省后续计算 }
优化 bump 计算
计算 bump 会消耗计算单元,推荐在初始化时存储 bump 值:
rustpub fn initialize(ctx: Context<CreateVault>) -> Result<()> { let vault = &mut ctx.accounts.vault; vault.authority = ctx.accounts.user.key(); vault.balance = 0; vault.bump = ctx.bumps.vault; // 存储 bump Ok(()) }
后续使用时直接引用存储的 bump:
rust#[account( seeds = [b"vault", authority.key().as_ref()], bump = vault.bump // 使用存储的 bump )] pub vault: Account<'info, Vault>,
账户空间计算
使用 #[derive(InitSpace)] 自动计算账户所需空间:
rust#[account] #[derive(InitSpace)] pub struct UserProfile { pub authority: Pubkey, // 32 字节 pub level: u8, // 1 字节 pub experience: u64, // 8 字节 #[max_len(50)] pub username: String, // 4 + 50 字节 pub created_at: i64, // 8 字节 } // 总空间:8(鉴别器) + 32 + 1 + 8 + 54 + 8 = 111 字节
对于动态类型,使用 #[max_len()] 指定最大长度:
rust#[max_len(100)] pub items: Vec<Pubkey>, // 4 + (32 * 100) 字节
账户重分配
当需要改变账户大小时,使用 realloc 约束:
rust#[account( mut, realloc = 8 + new_size, realloc::payer = authority, realloc::zero = false // 是否清零新空间 )] pub data_account: Account<'info, DynamicData>,
关闭账户
关闭不再需要的账户,回收租金:
rust#[account( mut, close = recipient // 将剩余 lamports 转给 recipient )] pub account_to_close: Account<'info, TemporaryData>, #[account(mut)] pub recipient: SystemAccount<'info>,
Token 账户
使用 anchor_spl 处理 SPL Token:
toml# Cargo.toml [dependencies] anchor-spl = "0.31.1"
rustuse anchor_spl::token::{Token, TokenAccount, Mint}; #[derive(Accounts)] pub struct TokenTransfer<'info> { #[account( mut, associated_token::mint = mint, associated_token::authority = sender, )] pub sender_ata: Account<'info, TokenAccount>, #[account( init_if_needed, payer = sender, associated_token::mint = mint, associated_token::authority = recipient, )] pub recipient_ata: Account<'info, TokenAccount>, pub mint: Account<'info, Mint>, #[account(mut)] pub sender: Signer<'info>, /// CHECK: 接收方无需签名 pub recipient: UncheckedAccount<'info>, pub token_program: Program<'info, Token>, pub associated_token_program: Program<'info, AssociatedToken>, pub system_program: Program<'info, System>, }
兼容 Token-2022
使用 Interface 类型同时支持 Token 和 Token-2022:
rustuse anchor_spl::token_interface::{TokenInterface, TokenAccount, Mint}; #[derive(Accounts)] pub struct UniversalTransfer<'info> { #[account(mut)] pub source: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub destination: InterfaceAccount<'info, TokenAccount>, pub mint: InterfaceAccount<'info, Mint>, pub token_program: Interface<'info, TokenInterface>, }
惰性账户(LazyAccount)
从 Anchor 0.31.0 开始,LazyAccount 提供了更高效的账户读取方式:
toml# Cargo.toml [dependencies] anchor-lang = { version = "0.31.1", features = ["lazy-account"] }
rust#[derive(Accounts)] pub struct ReadData<'info> { pub account: LazyAccount<'info, LargeData>, } pub fn read_specific_field(ctx: Context<ReadData>) -> Result<()> { // 只加载需要的字段,节省计算资源 let value = ctx.accounts.account.get_value()?; msg!("Value: {}", value); Ok(()) }
LazyAccount 只使用 24 字节栈内存,适合处理大型账户。
剩余账户(Remaining Accounts)
处理数量不固定的账户:
rust#[derive(Accounts)] pub struct BatchTransfer<'info> { #[account(mut)] pub source: Account<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Program<'info, Token>, // 其他账户通过 remaining_accounts 传入 } pub fn batch_transfer(ctx: Context<BatchTransfer>, amounts: Vec<u64>) -> Result<()> { let remaining = &ctx.remaining_accounts; // 验证账户数量匹配 require!( remaining.len() == amounts.len(), CustomError::AccountCountMismatch ); for (i, account_info) in remaining.iter().enumerate() { // 验证并处理每个账户 let destination = Account::<TokenAccount>::try_from(account_info)?; // 执行转账... } Ok(()) }