Cấu trúc chương trình Rust
Các chương trình Solana được viết bằng Rust có yêu cầu cấu trúc tối thiểu, cho
phép linh hoạt trong cách tổ chức mã. Yêu cầu duy nhất là chương trình phải có
một entrypoint
, định nghĩa nơi bắt đầu thực thi của chương trình.
Cấu trúc chương trình
Mặc dù không có quy tắc nghiêm ngặt cho cấu trúc tệp, các chương trình Solana thường tuân theo một mẫu phổ biến:
entrypoint.rs
: Định nghĩa entrypoint để định tuyến các lệnh đến.state.rs
: Định nghĩa trạng thái cụ thể của chương trình (dữ liệu tài khoản).instructions.rs
: Định nghĩa các lệnh mà chương trình có thể thực thi.processor.rs
: Định nghĩa các trình xử lý lệnh (hàm) thực hiện logic nghiệp vụ cho mỗi lệnh.error.rs
: Định nghĩa các lỗi tùy chỉnh mà chương trình có thể trả về.
Bạn có thể tìm thấy các ví dụ trong Solana Program Library.
Chương trình mẫu
Để minh họa cách xây dựng một chương trình Rust gốc với nhiều lệnh, chúng ta sẽ xem xét một chương trình đếm đơn giản thực hiện hai lệnh:
InitializeCounter
: Tạo và khởi tạo một tài khoản mới với giá trị ban đầu.IncrementCounter
: Tăng giá trị được lưu trữ trong một tài khoản hiện có.
Để đơn giản, chương trình sẽ được triển khai trong một tệp lib.rs
duy nhất,
mặc dù trong thực tế bạn có thể muốn chia các chương trình lớn hơn thành nhiều
tệp.
Tạo một chương trình mới
Đầu tiên, tạo một dự án Rust mới sử dụng lệnh cargo init
tiêu chuẩn với cờ
--lib
.
cargo init counter_program --lib
Di chuyển đến thư mục dự án. Bạn sẽ thấy các tệp src/lib.rs
và Cargo.toml
mặc định
cd counter_program
Tiếp theo, thêm dependency solana-program
. Đây là dependency tối thiểu cần
thiết để xây dựng một chương trình Solana.
cargo add solana-program@1.18.26
Tiếp theo, thêm đoạn mã sau vào Cargo.toml
. Nếu bạn không bao gồm cấu hình
này, thư mục target/deploy
sẽ không được tạo khi bạn xây dựng chương trình.
[lib]crate-type = ["cdylib", "lib"]
Tệp Cargo.toml
của bạn nên trông như sau:
[package]name = "counter_program"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "lib"][dependencies]solana-program = "1.18.26"
Điểm vào chương trình
Điểm vào của chương trình Solana là hàm được gọi khi một chương trình được kích hoạt. Điểm vào có định nghĩa cơ bản sau và các nhà phát triển có thể tự do tạo triển khai riêng của hàm điểm vào.
Để đơn giản, hãy sử dụng macro
entrypoint!
từ crate solana_program
để định nghĩa điểm vào trong chương trình của bạn.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Thay thế mã mặc định trong lib.rs
bằng đoạn mã sau. Đoạn mã này:
- Import các dependency cần thiết từ
solana_program
- Định nghĩa điểm vào chương trình sử dụng macro
entrypoint!
- Triển khai hàm
process_instruction
sẽ định tuyến các chỉ thị đến các hàm xử lý thích hợp
use solana_program::{account_info::{next_account_info, AccountInfo},entrypoint,entrypoint::ProgramResult,msg,program::invoke,program_error::ProgramError,pubkey::Pubkey,system_instruction,sysvar::{rent::Rent, Sysvar},};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}
Macro entrypoint!
yêu cầu một hàm với
kiểu chữ ký
sau đây làm đối số:
pub type ProcessInstruction =fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Khi một chương trình Solana được gọi, điểm vào
giải mã
dữ liệu đầu vào
(được cung cấp dưới dạng byte) thành ba giá trị và truyền chúng vào hàm
process_instruction
:
program_id
: Khóa công khai của chương trình đang được gọi (chương trình hiện tại)accounts
:AccountInfo
cho các tài khoản cần thiết bởi chỉ thị đang được gọiinstruction_data
: Dữ liệu bổ sung được truyền vào chương trình để chỉ định chỉ thị cần thực thi và các đối số cần thiết của nó
Ba tham số này trực tiếp tương ứng với dữ liệu mà các client phải cung cấp khi xây dựng một chỉ thị để gọi một chương trình.
Định nghĩa trạng thái chương trình
Khi xây dựng một chương trình Solana, bạn thường bắt đầu bằng việc định nghĩa trạng thái của chương trình - dữ liệu sẽ được lưu trữ trong các tài khoản được tạo và sở hữu bởi chương trình của bạn.
Trạng thái chương trình được định nghĩa bằng cách sử dụng các struct Rust đại diện cho cấu trúc dữ liệu của các tài khoản trong chương trình của bạn. Bạn có thể định nghĩa nhiều struct để đại diện cho các loại tài khoản khác nhau cho chương trình của bạn.
Khi làm việc với các tài khoản, bạn cần một cách để chuyển đổi các kiểu dữ liệu của chương trình thành và từ các byte thô được lưu trữ trong trường dữ liệu của tài khoản:
- Serialization: Chuyển đổi các kiểu dữ liệu của bạn thành các byte để lưu trữ trong trường dữ liệu của tài khoản
- Deserialization: Chuyển đổi các byte được lưu trữ trong tài khoản trở lại thành các kiểu dữ liệu của bạn
Mặc dù bạn có thể sử dụng bất kỳ định dạng serialization nào cho việc phát triển chương trình Solana, Borsh thường được sử dụng. Để sử dụng Borsh trong chương trình Solana của bạn:
- Thêm crate
borsh
như một dependency vàoCargo.toml
của bạn:
cargo add borsh
- Import các trait Borsh và sử dụng derive macro để triển khai các trait cho các struct của bạn:
use borsh::{BorshSerialize, BorshDeserialize};// Define struct representing our counter account's data#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Thêm struct CounterAccount
vào lib.rs
để định nghĩa trạng thái chương trình.
Struct này sẽ được sử dụng trong cả hai instruction khởi tạo và tăng giá trị.
use solana_program::{account_info::{next_account_info, AccountInfo},entrypoint,entrypoint::ProgramResult,msg,program::invoke,program_error::ProgramError,pubkey::Pubkey,system_instruction,sysvar::{rent::Rent, Sysvar},};use borsh::{BorshSerialize, BorshDeserialize};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Định nghĩa các instruction
Instruction đề cập đến các hoạt động khác nhau mà chương trình Solana của bạn có thể thực hiện. Hãy coi chúng như các API công khai cho chương trình của bạn - chúng định nghĩa những hành động mà người dùng có thể thực hiện khi tương tác với chương trình của bạn.
Instruction thường được định nghĩa bằng cách sử dụng một enum Rust trong đó:
- Mỗi biến thể enum đại diện cho một instruction khác nhau
- Payload của biến thể đại diện cho các tham số của instruction
Lưu ý rằng các biến thể enum trong Rust được đánh số ngầm định bắt đầu từ 0.
Dưới đây là một ví dụ về enum định nghĩa hai instruction:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
Khi một client gọi chương trình của bạn, họ phải cung cấp instruction data (dưới dạng buffer byte) trong đó:
- Byte đầu tiên xác định biến thể instruction nào sẽ được thực thi (0, 1, v.v.)
- Các byte còn lại chứa tham số instruction được tuần tự hóa (nếu cần thiết)
Để chuyển đổi instruction data (các byte) thành một biến thể của enum, thông thường người ta triển khai một phương thức hỗ trợ. Phương thức này:
- Tách byte đầu tiên để lấy biến thể instruction
- Khớp với biến thể và phân tích cú pháp các tham số bổ sung từ các byte còn lại
- Trả về biến thể enum tương ứng
Ví dụ, phương thức unpack
cho enum CounterInstruction
:
impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Thêm đoạn mã sau vào lib.rs
để định nghĩa các instruction cho chương trình
counter.
use borsh::{BorshDeserialize, BorshSerialize};use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,program_error::ProgramError, pubkey::Pubkey,};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?,);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Trình xử lý Instruction
Trình xử lý instruction là các hàm chứa logic nghiệp vụ cho mỗi instruction.
Thông thường người ta đặt tên các hàm xử lý là process_<instruction_name>
,
nhưng bạn có thể tự do chọn bất kỳ quy ước đặt tên nào.
Thêm đoạn mã sau vào lib.rs
. Đoạn mã này sử dụng enum CounterInstruction
và
phương thức unpack
đã định nghĩa ở bước trước để định tuyến các instruction
đến các hàm xử lý thích hợp:
entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Unpack instruction datalet instruction = CounterInstruction::unpack(instruction_data)?;// Match instruction typematch instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?,};}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {// Implementation details...Ok(())}fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {// Implementation details...Ok(())}
Tiếp theo, thêm triển khai của hàm process_initialize_counter
. Trình xử lý
instruction này:
- Tạo và phân bổ không gian cho một tài khoản mới để lưu trữ dữ liệu counter
- Khởi tạo dữ liệu tài khoản với
initial_value
được truyền vào instruction
// Initialize a new counter accountfn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;let payer_account = next_account_info(accounts_iter)?;let system_program = next_account_info(accounts_iter)?;// Size of our counter accountlet account_space = 8; // Size in bytes to store a u64// Calculate minimum balance for rent exemptionlet rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);// Create the counter accountinvoke(&system_instruction::create_account(payer_account.key, // Account paying for the new accountcounter_account.key, // Account to be createdrequired_lamports, // Amount of lamports to transfer to the new accountaccount_space as u64, // Size in bytes to allocate for the data fieldprogram_id, // Set program owner to our program),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;// Create a new CounterAccount struct with the initial valuelet counter_data = CounterAccount {count: initial_value,};// Get a mutable reference to the counter account's datalet mut account_data = &mut counter_account.data.borrow_mut()[..];// Serialize the CounterAccount struct into the account's datacounter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
Tiếp theo, thêm phần triển khai của hàm process_increment_counter
. Instruction
này tăng giá trị của một tài khoản bộ đếm hiện có.
// Update an existing counter's valuefn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;// Verify account ownershipif counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}// Mutable borrow the account datalet mut data = counter_account.data.borrow_mut();// Deserialize the account data into our CounterAccount structlet mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;// Increment the counter valuecounter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;// Serialize the updated counter data back into the accountcounter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
Kiểm thử instruction
Để kiểm thử các instruction của chương trình, hãy thêm các dependency sau vào
Cargo.toml
.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
Sau đó thêm module kiểm thử sau vào lib.rs
và chạy cargo test-sbf
để thực
thi các bài kiểm thử. Tùy chọn, sử dụng cờ --nocapture
để xem các câu lệnh
print trong đầu ra.
cargo test-sbf -- --nocapture
#[cfg(test)]mod test {use super::*;use solana_program_test::*;use solana_sdk::{instruction::{AccountMeta, Instruction},signature::{Keypair, Signer},system_program,transaction::Transaction,};#[tokio::test]async fn test_counter_program() {let program_id = Pubkey::new_unique();let (mut banks_client, payer, recent_blockhash) = ProgramTest::new("counter_program",program_id,processor!(process_instruction),).start().await;// Create a new keypair to use as the address for our counter accountlet counter_keypair = Keypair::new();let initial_value: u64 = 42;// Step 1: Initialize the counterprintln!("Testing counter initialization...");// Create initialization instructionlet mut init_instruction_data = vec![0]; // 0 = initialize instructioninit_instruction_data.extend_from_slice(&initial_value.to_le_bytes());let initialize_instruction = Instruction::new_with_bytes(program_id,&init_instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true),AccountMeta::new(payer.pubkey(), true),AccountMeta::new_readonly(system_program::id(), false),],);// Send transaction with initialize instructionlet mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("✅ Counter initialized successfully with value: {}",counter.count);}// Step 2: Increment the counterprintln!("Testing counter increment...");// Create increment instructionlet increment_instruction = Instruction::new_with_bytes(program_id,&[1], // 1 = increment instructionvec![AccountMeta::new(counter_keypair.pubkey(), true)],);// Send transaction with increment instructionlet mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("✅ Counter incremented successfully to: {}", counter.count);}}}
Kết quả đầu ra ví dụ:
running 1 test[2024-10-29T20:51:13.783708000Z INFO solana_program_test] "counter_program" SBF program from /counter_program/target/deploy/counter_program.so, modified 2 seconds, 169 ms, 153 µs and 461 ns ago[2024-10-29T20:51:13.855204000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.856052000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2][2024-10-29T20:51:13.856135000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success[2024-10-29T20:51:13.856242000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter initialized with value: 42[2024-10-29T20:51:13.856285000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 3791 of 200000 compute units[2024-10-29T20:51:13.856307000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success[2024-10-29T20:51:13.860038000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.860333000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter incremented to: 43[2024-10-29T20:51:13.860355000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 756 of 200000 compute units[2024-10-29T20:51:13.860375000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM successtest test::test_counter_program ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
Is this page helpful?