Anchor
Anchor cho người mới bắt đầu

Anchor cho người mới bắt đầu

Anchor nâng cao

Thỉnh thoảng, sự trừu tượng được thực hiện với Anchor khiến cho việc xây dựng logic mà chương trình của chúng ta cần trở nên không thể. Vì lý do đó, trong phần này, chúng ta sẽ nói về cách sử dụng một số khái niệm nâng cao để làm việc với chương trình của mình.

Các cờ tính năng

Các kỹ sư phần mềm thường xuyên cần các môi trường khác nhau cho phát triển cục bộ, kiểm thử và sản xuất. Cờ tính năng cung cấp một giải pháp tốt bằng cách cho phép biên dịch có điều kiện và cấu hình cụ thể cho môi trường mà không cần duy trì các mã nguồn riêng biệt.

Các tính năng của Cargo

Các tính năng của Cargo cung cấp một cơ chế mạnh mẽ cho việc biên dịch có điều kiện và các phụ thuộc tùy chọn. Bạn định nghĩa tên các tính năng trong phần [features] của Cargo.toml, sau đó kích hoạt hoặc vô hiệu hóa chúng khi cần:

  • Kích hoạt các tính năng qua dòng lệnh: --features feature_name

  • Kích hoạt các tính năng cho các phụ thuộc trực tiếp trong Cargo.toml

Điều này cho phép bạn kiểm soát chi tiết những gì được bao gồm trong tệp nhị phân cuối cùng của bạn.

Cờ chức năng trong Anchor

Các chương trình Anchor thường sử dụng cờ chức năng để tạo ra các hành vi, ràng buộc hoặc cấu hình khác nhau dựa trên môi trường mục tiêu. Sử dụng thuộc tính cfg để biên dịch mã có điều kiện:

rust
#[cfg(feature = "testing")]
fn function_for_testing() {
   // Compiled only when "testing" feature is enabled
}

#[cfg(not(feature = "testing"))]
fn function_for_production() {
   // Compiled only when "testing" feature is disabled
}

Các cờ chức năng rất hiệu quả trong việc xử lý sự khác biệt giữa các môi trường. Vì không phải tất cả các token đều được triển khai trên Mainnet và Devnet, bạn thường cần các địa chỉ khác nhau cho các mạng khác nhau:

rust
#[cfg(feature = "localnet")]
pub const TOKEN_ADDRESS: &str = "Local_Token_Address_Here";

#[cfg(not(feature = "localnet"))]
pub const TOKEN_ADDRESS: &str = "Mainnet_Token_Address_Here";

Cách tiếp cận này loại bỏ các lỗi cấu hình triển khai và hợp lý hóa quy trình phát triển của bạn bằng cách chuyển đổi môi trường tại thời điểm biên dịch thay vì thời điểm chạy.

Đây là cách bạn thiết lập nó trong tệp Cargo.toml của mình:

toml
[features]
default = ["localnet"]
localnet = []

Sau đó, bạn có thể chỉ định cờ mà bạn muốn khi build chương trình của mình như thế này:

text
# Uses default (localnet)
anchor build

# Build for mainnet
anchor build --no-default-features

Sau khi bạn build chương trình, tệp nhị phân sẽ chỉ có cờ điều kiện mà bạn đã kích hoạt, có nghĩa là khi bạn kiểm thử và triển khai, nó sẽ tôn trọng điều kiện đó.

Làm việc với Tài khoản Thô

Anchor giúp đơn giản hóa việc xử lý tài khoản bằng cách tự động deserialize các tài khoản thông qua ngữ cảnh của nó:

rust
#[derive(Accounts)]
pub struct Instruction<'info> {
    pub account: Account<'info, MyAccount>,
}

#[account]
pub struct MyAccount {
    pub data: u8,
}

Tuy nhiên, việc tự động deserialize này trở thành có vấn đề khi bạn cần xử lý tài khoản có điều kiện, chẳng hạn như deserialize và sửa đổi một tài khoản chỉ khi các tiêu chí cụ thể được đáp ứng.

Đối với các kịch bản có điều kiện, hãy sử dụng UncheckedAccount để hoãn việc xác thực và deserialize cho đến thời điểm chạy. Điều này ngăn chặn các lỗi nghiêm trọng khi các tài khoản có thể không tồn tại hoặc khi bạn cần xác thực chúng theo cách lập trình:

rust
#[derive(Accounts)]
pub struct Instruction<'info> {
    /// CHECK: Validated conditionally in instruction logic
    pub account: UncheckedAccount<'info>,
}

#[account]
pub struct MyAccount {
    pub data: u8,
}

pub fn instruction(ctx: Context<Instruction>, should_process: bool) -> Result<()> {
    if should_process {
        // Deserialize the account data
        let mut account = MyAccount::try_deserialize(&mut &ctx.accounts.account.to_account_info().data.borrow_mut()[..])
            .map_err(|_| error!(InstructionError::AccountNotFound))?;

        // Modify the account data
        account.data += 1;

        // Serialize back the data to the account
        account.try_serialize(&mut &mut ctx.accounts.account.to_account_info().data.borrow_mut()[..])?;
    }

    Ok(())
}

Các zero copy Accounts

Runtime của Solana yêu cầu giới hạn bộ nhớ nghiêm ngặt: 4KB cho bộ nhớ ngăn xếp và 32KB cho bộ nhớ heap. Ngoài ra, ngăn xếp tăng thêm 10KB cho mỗi tài khoản được tải. Những ràng buộc này khiến việc deserialize truyền thống trở nên không khả thi đối với các tài khoản lớn, yêu cầu các kỹ thuật zero-copy để quản lý bộ nhớ hiệu quả.

Khi các tài khoản vượt quá các giới hạn này, bạn sẽ gặp phải các lỗi tràn ngăn xếp như: Stack offset of -30728 exceeded max offset of -4096 by 26632 bytes

Đối với các tài khoản có kích thước trung bình, bạn có thể sử dụng Box để di chuyển dữ liệu từ ngăn xếp sang heap (như đã đề cập trong phần giới thiệu), nhưng các tài khoản lớn hơn yêu cầu triển khai zero_copy.

Zero-copy bỏ qua việc tự động deserialize toàn bộ bằng cách sử dụng truy cập bộ nhớ thô. Để định nghĩa một loại tài khoản sử dụng zero-copy, hãy chú thích cấu trúc với #[account(zero_copy)]:

rust
#[account(zero_copy)]
pub struct Data {
    // 10240 bytes - 8 bytes account discriminator
    pub data: [u8; 10_232],
}

Thuộc tính #[account(zero_copy)] tự động triển khai một vài traits cần thiết cho việc deserialization zero-copy:

  • #[derive(Copy, Clone)],

  • #[derive(bytemuck::Zeroable)],

  • #[derive(bytemuck::Pod)], và

  • #[repr(C)]

Ghi chú: để sử dụng zero-copy, hãy thêm crate bytemuck vào các dependencies của bạn với tính năng min_const_generics để làm việc với các mảng có bất kỳ kích thước nào trong các kiểu zero-copy của bạn.

Để deserialize một tài khoản zero-copy trong ngữ cảnh lệnh của bạn, hãy sử dụng AccountLoader<'info, T>, trong đó T là kiểu tài khoản zero-copy của bạn:

rust
#[derive(Accounts)]
pub struct Instruction<'info> {
    pub data_account: AccountLoader<'info, Data>,
}

Khởi tạo một zero copy account

Có hai cách tiếp cận khác nhau để khởi tạo, tùy thuộc vào kích thước tài khoản của bạn:

Đối với các tài khoản dưới 10.240 byte, hãy sử dụng ràng buộc init trực tiếp:

rust
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        // 10240 bytes is max space to allocate with init constraint
        space = 8 + 10_232,
        payer = payer,
    )]
    pub data_account: AccountLoader<'info, Data>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

Ràng buộc init giới hạn kích thước tối đa là 10.240 byte do các hạn chế của CPI. Ở mức độ cơ bản, init thực hiện một cuộc gọi CPI đến System Program để tạo tài khoản.

Đối với các tài khoản yêu cầu nhiều hơn 10.240 byte, bạn phải tạo tài khoản một cách riêng biệt bằng cách gọi System Program nhiều lần, thêm 10.240 byte cho mỗi giao dịch. Điều này cho phép bạn tạo các tài khoản lên đến kích thước tối đa của Solana là 10MB (10.485.760 byte), bỏ qua hạn chế CPI.

Sau khi tạo tài khoản bên ngoài, hãy sử dụng ràng buộc zero thay vì init. Ràng buộc zero xác minh rằng tài khoản chưa được khởi tạo bằng cách kiểm tra rằng discriminator của nó chưa được thiết lập:

rust
#[account(zero_copy)]
pub struct Data {
    // 10,485,780 bytes - 8 bytes account discriminator
    pub data: [u8; 10_485_752],
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(zero)]
    pub data_account: AccountLoader<'info, Data>,
}

Đối với cả 2 phương pháp khởi tạo, hãy gọi load_init() để có được một tham chiếu có thể thay đổi đến dữ liệu tài khoản và thiết lập discriminator của tài khoản:

rust
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let account = &mut ctx.accounts.data_account.load_init()?;

    // Set your account data here
    // account.data = something;

    Ok(())
}

Tải một zero copy account

Khi đã được khởi tạo, hãy sử dụng load() để đọc dữ liệu tài khoản:

rust
#[derive(Accounts)]
pub struct ReadOnly<'info> {
    pub data_account: AccountLoader<'info, Data>,
}

pub fn read_only(ctx: Context<ReadOnly>) -> Result<()> {
    let account = &ctx.accounts.data_account.load()?;

    // Read your data here
    // let value = account.data;
    
    Ok(())
}

Làm việc với các CPI thô

Anchor trừu tượng hóa sự phức tạp của cross-program invocation (CPI), nhưng hiểu các cơ chế cơ bản là rất quan trọng cho phát triển Solana nâng cao.

Mỗi instruction bao gồm ba thành phần chính: program_id, mảng accounts và các byte instruction_data mà Solana Runtime xử lý thông qua syscall sol_invoke.

Ở cấp độ hệ thống, Solana thực hiện CPIs thông qua syscall này:

rust
/// Solana BPF syscall for invoking a signed instruction.
fn sol_invoke_signed_c(
    instruction_addr: *const u8,
    account_infos_addr: *const u8,
    account_infos_len: u64,
    signers_seeds_addr: *const u8,
    signers_seeds_len: u64,
) -> u64;

Runtime nhận con trỏ đến dữ liệu của instruction và thông tin các account của bạn, sau đó thực thi chương trình mục tiêu với các đầu vào này.

Đây là cách bạn có thể gọi một chương trình Anchor bằng cách sử dụng các nguyên thủy Solana thô:

rust
pub fn manual_cpi(ctx: Context<MyCpiContext>) -> Result<()> {
    // Construct instruction discriminator (8-byte SHA256 hash prefix)
    let discriminator = sha256("global:instruction_name")[0..8]
    
    // Build complete instruction data
    let mut instruction_data = discriminator.to_vec();
    instruction_data.extend_from_slice(&[additional_instruction_data]); // Your instruction parameters
    
    // Define account metadata for the target program
    let accounts = vec![
        AccountMeta::new(ctx.accounts.account_1.key(), true),           // Signer + writable
        AccountMeta::new_readonly(ctx.accounts.account_2.key(), false), // Read-only
        AccountMeta::new(ctx.accounts.account_3.key(), false),          // Writable
    ];
    
    // Collect account infos for the syscall
    let account_infos = vec![
        ctx.accounts.account_1.to_account_info(),
        ctx.accounts.account_2.to_account_info(),
        ctx.accounts.account_3.to_account_info(),
    ];
    
    // Create the instruction
    let instruction = solana_program::instruction::Instruction {
        program_id: target_program::ID,
        accounts,
        data: instruction_data,
    };
    
    // Execute the CPI
    solana_program::program::invoke(&instruction, &account_infos)?;
    
    Ok(())
}

// For PDA-signed CPIs, use invoke_signed instead:
pub fn pda_signed_cpi(ctx: Context<PdaCpiContext>) -> Result<()> {
    // ... instruction construction same as above ...
    
    let signer_seeds = &[
        b"seed",
        &[bump],
    ];
    
    solana_program::program::invoke_signed(
        &instruction,
        &account_infos,
        &[signer_seeds],
    )?;
    
    Ok(())
}

CPI đến một chương trình Anchor khác

Macro declare_program!() của Anchor cho phép gọi chéo chương trình một cách an toàn về kiểu mà không cần thêm chương trình mục tiêu vào như là một thành phần phụ thuộc. Macro này tạo ra các module Rust từ IDL của một chương trình, cung cấp các hàm bổ trợ CPI và các loại tài khoản cho các tương tác chương trình liền mạch.

Đặt tệp IDL của chương trình mục tiêu trong thư mục /idls ở bất kỳ đâu trong cấu trúc dự án của bạn:

text
project/
├── idls/
│   └── target_program.json
├── programs/
│   └── your_program/
└── Cargo.toml

Sau đó sử dụng macro để tạo ra các module cần thiết:

rust
use anchor_lang::prelude::*;

declare_id!("YourProgramID");

// Generate modules from IDL
declare_program!(target_program);

// Import the generated types
use target_program::{
    accounts::Counter,             // Account types
    cpi::{self, accounts::*},      // CPI functions and account structs  
    program::TargetProgram,        // Program type for validation
};

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

    pub fn call_other_program(ctx: Context<CallOtherProgram>) -> Result<()> {
        // Create CPI context
        let cpi_ctx = CpiContext::new(
            ctx.accounts.target_program.to_account_info(),
            Initialize {
                payer: ctx.accounts.payer.to_account_info(),
                counter: ctx.accounts.counter.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
            },
        );

        // Execute the CPI using generated helper
        target_program::cpi::initialize(cpi_ctx)?;
        
        Ok(())
    }
}

#[derive(Accounts)]
pub struct CallOtherProgram<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut)]
    pub counter: Account<'info, Counter>,  // Uses generated account type    
    pub target_program: Program<'info, TargetProgram>,
    pub system_program: Program<'info, System>,
}

Bạn có thể gọi CPI đến chương trình Anchor khác bằng cách thêm vào Cargo.toml của bạn dưới [dependencies]: callee = { path = "../callee", features = ["cpi"] } sau đó build chương trình của bạn bằng anchor build -- --features cpi và sử dụng callee::cpi::<instruction>(). Điều này không được khuyến khích vì nó có thể gây ra lỗi phụ thuộc lòng vòng.

Nội dung
Xem mã nguồn
Blueshift © 2025Commit: e573eab