Các tài khoản
Chúng ta đã thấy macro #[account]
, nhưng tự nhiên trên Solana có nhiều loại tài khoản khác nhau. Vì lý do này, đáng để dành một chút thời gian để xem cách các tài khoản trên Solana hoạt động nói chung, nhưng sâu hơn, cách chúng hoạt động với Anchor.
Tổng quan chung
Trên Solana, mọi phần của trạng thái đều tồn tại trong một tài khoản; hãy hình dung ledger như một bảng khổng lồ nơi mỗi hàng chia sẻ cùng một schema cơ bản:
pub struct Account {
/// lamports in the account
pub lamports: u64,
/// data held in this account
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
pub data: Vec<u8>,
/// the program that owns this account and can mutate its lamports or data.
pub owner: Pubkey,
/// `true` if the account is a program; `false` if it merely belongs to one
pub executable: bool,
/// the epoch at which this account will next owe rent (currently deprecated and is set to `0`)
pub rent_epoch: Epoch,
}
Tất cả các tài khoản trên Solana đều chia sẻ cùng một layout cơ bản. Điều khiến chúng khác biệt là:
- Owner: Chương trình có đặc quyền để sửa đổi dữ liệu và lamport của tài khoản.
- Dữ liệu: Được sử dụng bởi chương trình owner để phân biệt giữa các loại tài khoản khác nhau.
Khi chúng ta nói về Token Program Accounts, ý chúng ta là một tài khoản nơi owner
là Token Program. Không giống như System Account có trường data trống, Token Program Account có thể là tài khoản Mint hoặc Token. Chúng ta sử dụng discriminator để phân biệt giữa chúng.
Cũng như Token Program có thể sở hữu tài khoản, bất kỳ chương trình nào khác thậm chí cả chương trình của chúng ta cũng có thể.
Các tài khoản của chương trình
Các tài khoản của chương trình là nền tảng của quản lý trạng thái trong các chương trình Anchor. Chúng cho phép bạn tạo các cấu trúc dữ liệu tùy chỉnh được sở hữu bởi chương trình của bạn. Hãy khám phá cách làm việc với chúng một cách hiệu quả.
Cấu trúc Account và Discriminator
Mỗi tài khoản chương trình trong Anchor cần một cách để xác định loại của nó. Điều này được xử lý thông qua discriminator, có thể là:
- Discriminator Mặc định: Một prefix 8-byte được tạo bằng cách
sha256("account:<StructName>")[0..8]
cho account, hoặcsha256("global:<instruction_name>")[0..8]
cho instruction. Các seed sử dụng PascalCase cho account và snake_case cho instruction.
- Discriminator tùy chỉnh: Bắt đầu từ Anchor
v0.31.0
, bạn có thể chỉ định discriminator của riêng mình:
#[account(discriminator = 1)] // single-byte
pub struct Escrow { … }
Lưu ý quan trọng về Discriminator:
- Chúng phải là duy nhất trong chương trình của bạn
- Sử dụng
[1]
ngăn cản việc sử dụng[1, 2, …]
vì chúng cũng bắt đầu bằng1
[0]
không thể được sử dụng vì nó xung đột với các tài khoản chưa được khởi tạo
Tạo tài khoản của chương trình
Để tạo một tài khoản của chương trình, trước tiên bạn định nghĩa cấu trúc dữ liệu của mình:
use anchor_lang::prelude::*;
#[derive(InitSpace)]
#[account(discriminator = 1)]
pub struct CustomAccountType {
data: u64,
}
Các điểm chính về tài khoản chương trình:
- Kích thước tối đa là 10,240 byte (10 KiB)
- Đối với các tài khoản lớn hơn, bạn sẽ cần
zero_copy
và ghi từng đoạn - Macro derive
InitSpace
tự động tính toán không gian cần thiết cho tài khoản - Tổng không gian =
INIT_SPACE
+DISCRIMINATOR.len()
Tổng không gian tính bằng byte cần thiết cho tài khoản là tổng của INIT_SPACE
(kích thước của tất cả các trường kết hợp) và kích thước discriminator (DISCRIMINATOR.len()
).
Các tài khoản Solana yêu cầu một khoản tiền gửi rent tính bằng lamport, phụ thuộc vào kích thước của tài khoản. Biết kích thước giúp chúng ta tính toán cần bao nhiêu lamport cần để mở tài khoản.
Đây là cách chúng ta sẽ khởi tạo tài khoản trong struct Account
của mình:
#[account(
init,
payer = <target_account>,
space = <num_bytes> // CustomAccountType::INIT_SPACE + CustomAccountType::DISCRIMINATOR.len(),
)]
pub account: Account<'info, CustomAccountType>,
Đây là một số trường được sử dụng trong macro #[account]
, ngoài các trường seeds
và bump
chúng ta đã đề cập trước đó, và những gì chúng làm:
init
: nói với Anchor rằng hãy tạo tài khoản này.payer
: người ký nào sẽ trả phí thuê cho tài khoản (ở đây, là maker).space
: số lượng byte cần được cấp phát cho tài khoản. Đây cũng là nơi tính toán phí thuê xảy ra.
Sau khi tạo, bạn có thể sửa đổi dữ liệu của tài khoản. Nếu bạn cần thay đổi kích thước của nó, hãy sử dụng realloc:
#[account(
mut, // Mark as mutable
realloc = <space>, // New size
realloc::payer = <target>, // Who pays for the change
realloc::zero = <bool> // Whether to zero new space
)]
Chú ý: Khi giảm kích thước tài khoản, hãy đặt realloc::zero = true
để đảm bảo rằng dữ liệu cũ được xóa đúng cách.
Cuối cùng, khi tài khoản không còn cần thiết nữa, chúng ta có thể đóng nó để thu hồi tiền thuê:
#[account(
mut, // Mark as mutable
close = <target_account>, // Where to send remaining lamports
)]
pub account: Account<'info, CustomAccountType>,
Chúng ta có thể thêm các PDA, các địa chỉ được xác định từ seed và ID của 1 chương trình mà nó đặc biệt hưu dụng cho việc tạo ra các địa chỉ tài khoản có thể dự đoán được, vào các ràng buộc như thế này:
#[account(
seeds = <seeds>, // Seeds for derivation
bump // Standard bump seed
)]
pub account: Account<'info, CustomAccountType>,
Chú ý: Các PDA là xác định: cùng một seeds + program + bump luôn luôn tạo ra cùng một địa chỉ và bump đảm bảo rằng địa chỉ nằm ngoài đường cong ed25519.
Bởi vì việc tính toán số bump có thể "đôt" nhiều tài nguyên CU, nên luôn tốt hơn là lưu trữ nó trong tài khoản hoặc chuyển nó vào trong lệnh và xác minh nó mà không cần phải tính toán như thế này:
#[account(
seeds = <seeds>,
bump = <expr>
)]
pub account: Account<'info, CustomAccountType>,
Và có thể tạo ra một PDA được tạo từ một chương trình khác bằng cách truyền địa chỉ của chương trình mà nó được tạo ra như thế này:
#[account(
seeds = <seeds>,
bump = <expr>,
seeds::program = <expr>
)]
pub account: Account<'info, CustomAccountType>,
Token Accounts
Token Program, một phần của Solana Program Library (SPL), là bộ công cụ tích hợp để mint và di chuyển bất kỳ tài sản nào không phải là SOL gốc. Nó có các instruction để tạo token, mint supply mới, chuyển số dư, burn, freeze, và nhiều hơn nữa.
Chương trình này sở hữu hai loại tài khoản chính:
- Mint Account: lưu trữ metadata cho một token cụ thể: supply, decimals, mint authority, freeze authority, và nhiều hơn nữa
- Token Account: giữ số dư của mint đó cho một owner cụ thể. Chỉ owner mới có thể giảm số dư (transfer, burn, v.v.), nhưng bất kỳ ai cũng có thể gửi token đến tài khoản, tăng số dư của nó
Token Accounts trong Anchor
Về bản chất, crate Anchor cốt lõi chỉ đóng gói các helper CPI và Accounts cho System Program. Nếu bạn muốn sự hỗ trợ tương tự cho SPL token, bạn cần kéo vào crate anchor_spl
.
anchor_spl
thêm:
- Helper builder cho mọi instruction trong cả chương trình SPL Token và Token-2022
- Type wrapper giúp việc xác minh và deserialize Mint và Token account trở nên dễ dàng
Hãy xem cách các tài khoản Mint
và Token
được cấu trúc:
#[account(
mint::decimals = <expr>,
mint::authority = <target_account>,
mint::freeze_authority = <target_account>
mint::token_program = <target_account>
)]
pub mint: Account<'info, Mint>,
#[account(
mut,
associated_token::mint = <target_account>,
associated_token::authority = <target_account>,
associated_token::token_program = <target_account>
)]
pub maker_ata_a: Account<'info, TokenAccount>,
Account<'info, Mint>
và Account<'info, TokenAccount>
yêu cầu Anchor:
- xác nhận tài khoản thực sự là Mint hoặc Token account
- deserialize dữ liệu của nó để bạn có thể đọc các trường trực tiếp
- thực thi bất kỳ ràng buộc bổ sung nào bạn chỉ định (
authority
,decimals
,mint
,token_program
, v.v.)
Các tài khoản liên quan đến token này tuân theo cùng pattern init
đã sử dụng trước đó. Vì Anchor biết kích thước byte cố định của chúng, chúng ta không cần chỉ định giá trị space
, chỉ cần payer tài trợ cho tài khoản.
Anchor cũng cung cấp macro init_if_needed
: nó kiểm tra xem token account đã tồn tại chưa và, nếu chưa, sẽ tạo nó. Điều đó không an toàn cho mọi loại tài khoản, nhưng nó hoàn toàn phù hợp với token account, vì vậy chúng ta sẽ dựa vào nó ở đây.
Như đã đề cập, anchor_spl
tạo helper cho cả chương trình Token và Token2022, với chương trình sau giới thiệu Token Extensions. Thách thức chính là mặc dù những tài khoản này đạt được mục tiêu tương tự và có cấu trúc tương đương, chúng không thể được deserialize và kiểm tra theo cách giống nhau vì chúng được sở hữu bởi hai chương trình khác nhau.
Chúng ta có thể tạo logic "nâng cao" hơn để xử lý các loại tài khoản khác nhau này, nhưng may mắn thay Anchor hỗ trợ tình huống này thông qua InterfaceAccounts:
use anchor_spl::token_interface::{Mint, TokenAccount};
#[account(
mint::decimals = <expr>,
mint::authority = <target_account>,
mint::freeze_authority = <target_account>
)]
pub mint: InterfaceAccounts<'info, Mint>,
#[account(
mut,
associated_token::mint = <target_account>,
associated_token::authority = <target_account>,
associated_token::token_program = <target_account>
)]
pub maker_ata_a: InterfaceAccounts<'info, TokenAccount>,
Sự khác biệt chính ở đây là chúng ta đang sử dụng InterfaceAccounts
thay vì Account
. Điều này cho phép chương trình của chúng ta hoạt động với cả Token và Token2022 account mà không cần xử lý sự khác biệt trong logic deserialization của chúng. Interface cung cấp một cách chung để tương tác với cả hai loại tài khoản trong khi duy trì type safety và validation thích hợp.
Cách tiếp cận này đặc biệt hữu ích khi bạn muốn chương trình của mình tương thích với cả hai tiêu chuẩn token, vì nó loại bỏ nhu cầu viết logic riêng biệt cho mỗi chương trình. Interface xử lý tất cả sự phức tạp của việc xử lý các cấu trúc tài khoản khác nhau ở hậu trường.
Nếu bạn muốn tìm hiểu thêm về cách sử dụng anchor-spl
, bạn có thể theo dõi các khóa học SPL-Token Program with Anchor hoặc Token2022 Program with Anchor.
Các Loại Account Bổ sung
Tất nhiên, System Account, Program Account và Token Account không phải là những loại tài khoản duy nhất mà chúng ta có thể có trong anchor. Vì vậy chúng ta sẽ xem ở đây các loại Account khác mà chúng ta có thể có:
Signer
Kiểu Signer
được sử dụng khi bạn cần xác minh rằng một tài khoản đã ký một transaction. Điều này rất quan trọng cho bảo mật vì nó đảm bảo rằng chỉ các tài khoản được ủy quyền mới có thể thực hiện các hành động nhất định. Bạn sẽ sử dụng kiểu này bất cứ khi nào bạn cần đảm bảo rằng một tài khoản cụ thể đã phê duyệt một transaction, chẳng hạn như khi chuyển tiền hoặc sửa đổi dữ liệu tài khoản yêu cầu quyền rõ ràng. Đây là cách bạn có thể sử dụng nó:
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
#[account(mut)]
pub signer: Signer<'info>,
}
Kiểu Signer
tự động kiểm tra xem tài khoản đã ký transaction chưa. Nếu chưa, transaction sẽ thất bại. Điều này đặc biệt hữu ích khi bạn cần đảm bảo rằng chỉ các tài khoản cụ thể mới có thể thực hiện các thao tác nhất định.
AccountInfo & UncheckedAccount
AccountInfo
và UncheckedAccount
là các kiểu tài khoản cấp thấp cung cấp quyền truy cập trực tiếp vào dữ liệu tài khoản mà không có validation tự động. Chúng giống hệt nhau về chức năng, nhưng UncheckedAccount
là lựa chọn được ưa thích vì tên của nó phản ánh tốt hơn mục đích của nó.
Những kiểu này hữu ích trong ba tình huống chính:
- Làm việc với các tài khoản thiếu cấu trúc được định nghĩa
- Triển khai logic validation tùy chỉnh
- Tương tác với các tài khoản từ chương trình khác không có định nghĩa kiểu Anchor
Vì những kiểu này bỏ qua các kiểm tra an toàn của Anchor, chúng vốn dĩ không an toàn và yêu cầu sự thừa nhận rõ ràng bằng cách sử dụng comment /// CHECK
. Comment này phục vụ như tài liệu rằng bạn hiểu rủi ro và đã triển khai validation thích hợp.
Đây là ví dụ về cách sử dụng chúng:
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
/// CHECK: This is an unchecked account
pub account: UncheckedAccount<'info>,
/// CHECK: This is an unchecked account
pub account_info: AccountInfo<'info>,
}
Option
Kiểu Option
trong Anchor cho phép bạn làm cho các tài khoản trở thành tùy chọn trong instruction của bạn. Khi một tài khoản được bao bọc trong Option
, nó có thể được cung cấp hoặc bỏ qua trong transaction. Điều này đặc biệt hữu ích cho:
- Xây dựng các instruction linh hoạt có thể hoạt động có hoặc không có các tài khoản nhất định
- Triển khai các tham số tùy chọn có thể không phải lúc nào cũng cần thiết
- Tạo các instruction tương thích ngược có thể hoạt động với cấu trúc tài khoản mới hoặc cũ
Khi một tài khoản Option
được đặt thành None
, Anchor sẽ sử dụng Program ID làm địa chỉ tài khoản. Hành vi này quan trọng để hiểu khi làm việc với các tài khoản tùy chọn.
Đây là cách triển khai nó:
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
pub optional_account: Option<Account<'info, CustomAccountType>>,
}
Box
Kiểu Box
được sử dụng để lưu trữ tài khoản trên heap thay vì stack. Điều này cần thiết trong một số tình huống:
- Khi xử lý các cấu trúc tài khoản lớn mà việc lưu trữ trên stack sẽ không hiệu quả
- Khi làm việc với các cấu trúc dữ liệu đệ quy
- Khi bạn cần làm việc với các tài khoản có kích thước không thể xác định tại thời điểm compile
Sử dụng Box
giúp quản lý bộ nhớ hiệu quả hơn trong những trường hợp này bằng cách cấp phát dữ liệu tài khoản trên heap. Đây là một ví dụ:
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
pub boxed_account: Box<Account<'info, LargeAccountType>>,
}
Program
Kiểu Program
được sử dụng để validate và tương tác với các chương trình Solana khác. Anchor có thể dễ dàng xác định các program account vì chúng có flag executable
được đặt thành true
. Kiểu này đặc biệt hữu ích khi:
- Bạn cần thực hiện Cross-Program Invocation (CPI)
- Bạn muốn đảm bảo rằng bạn đang tương tác với chương trình đúng
- Bạn cần xác minh quyền sở hữu chương trình của các tài khoản
Có hai cách chính để sử dụng kiểu Program
:
- Sử dụng các kiểu chương trình tích hợp (được khuyến nghị khi có sẵn):
use anchor_spl::token::Token;
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
}
- Sử dụng địa chỉ chương trình tùy chỉnh khi kiểu chương trình không có sẵn:
// Address of the Program
const PROGRAM_ADDRESS: Pubkey = pubkey!("22222222222222222222222222222222222222222222")
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
#[account(address = PROGRAM_ADDRESS)]
/// CHECK: this is fine since we're checking the address
pub program: UncheckedAccount<'info>,
}
Lưu ý: Khi làm việc với các token program, bạn có thể cần hỗ trợ cả Legacy Token Program và Token-2022 Program. Trong những trường hợp như vậy, hãy sử dụng kiểu Interface
thay vì Program
:
use anchor_spl::token_interface::TokenInterface;
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
pub program: Interface<'info, TokenInterface>,
}
Custom Account Validation
Anchor cung cấp một bộ ràng buộc mạnh mẽ có thể được áp dụng trực tiếp trong thuộc tính #[account]
. Những ràng buộc này giúp đảm bảo tính hợp lệ của tài khoản và thực thi các quy tắc chương trình ở cấp độ tài khoản, trước khi logic instruction của bạn chạy. Đây là các ràng buộc có sẵn:
Ràng buộc Address
Ràng buộc address
xác minh rằng public key của một tài khoản khớp với một giá trị cụ thể. Điều này rất quan trọng khi bạn cần đảm bảo rằng bạn đang tương tác với một tài khoản đã biết, chẳng hạn như một PDA cụ thể hoặc một tài khoản của chương trình:
#[account(
address = <expr>, // Basic usage
address = <expr> @ CustomError // With custom error
)]
pub account: Account<'info, CustomAccountType>,
Ràng buộc Owner
Ràng buộc owner
đảm bảo rằng một tài khoản được sở hữu bởi một chương trình cụ thể. Đây là một kiểm tra bảo mật quan trọng khi làm việc với các tài khoản thuộc sở hữu của chương trình, vì nó ngăn chặn truy cập trái phép vào các tài khoản mà lẽ ra phải được quản lý bởi một chương trình cụ thể:
#[account(
owner = <expr>, // Basic usage
owner = <expr> @ CustomError // With custom error
)]
pub account: Account<'info, CustomAccountType>,
Ràng buộc Executable
Ràng buộc executable
xác minh rằng một tài khoản là một program account (có flag executable
được đặt thành true
). Điều này đặc biệt hữu ích khi thực hiện Cross-Program Invocation (CPI) để đảm bảo rằng bạn đang tương tác với một chương trình thay vì một data account:
#[account(executable)]
pub account: Account<'info, CustomAccountType>,
Ràng buộc Mutable
Ràng buộc mut
đánh dấu một tài khoản là có thể thay đổi, cho phép dữ liệu của nó được sửa đổi trong quá trình thực hiện instruction. Điều này bắt buộc đối với bất kỳ tài khoản nào sẽ được cập nhật, vì Anchor thực thi tính bất biến theo mặc định để đảm bảo an toàn:
#[account(
mut, // Basic usage
mut @ CustomError // With custom error
)]
pub account: Account<'info, CustomAccountType>,
Ràng buộc Signer
Ràng buộc signer
xác minh rằng một tài khoản đã ký transaction. Điều này rất quan trọng cho bảo mật khi một tài khoản cần ủy quyền cho một hành động, chẳng hạn như chuyển tiền hoặc sửa đổi dữ liệu. Đây là một cách rõ ràng hơn để yêu cầu chữ ký so với việc sử dụng kiểu Signer
:
#[account(
signer, // Basic usage
signer @ CustomError // With custom error
)]
pub account: Account<'info, CustomAccountType>,
Ràng buộc Has One
Ràng buộc has_one
xác minh rằng một trường cụ thể trên account struct khớp với public key của tài khoản khác. Điều này hữu ích để duy trì mối quan hệ giữa các tài khoản, chẳng hạn như đảm bảo một token account thuộc về owner đúng:
#[account(
has_one = data @ Error::InvalidField
)]
pub account: Account<'info, CustomAccountType>,
Ràng buộc Custom
Khi các ràng buộc tích hợp không đáp ứng nhu cầu của bạn, bạn có thể viết một biểu thức validation tùy chỉnh. Điều này cho phép logic validation phức tạp không thể được biểu đạt bằng các ràng buộc khác, chẳng hạn như kiểm tra độ dài dữ liệu tài khoản hoặc xác thực mối quan hệ giữa nhiều trường:
#[account(
constraint = data == account.data @ Error::InvalidField
)]
pub account: Account<'info, CustomAccountType>,
Những ràng buộc này có thể được kết hợp để tạo ra các quy tắc validation mạnh mẽ cho tài khoản của bạn. Bằng cách đặt validation ở cấp độ tài khoản, bạn giữ các kiểm tra bảo mật gần với định nghĩa tài khoản và tránh việc rải rác các lời gọi require!()
trong suốt logic instruction của bạn.