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 điểm vào để định tuyến các chỉ thị đế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 chỉ thị mà chương trình có thể thực thi.processor.rs
: Định nghĩa các trình xử lý chỉ thị (hàm) thực hiện logic nghiệp vụ cho mỗi chỉ thị.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 Thư viện chương trình Solana.
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 chỉ thị, chúng ta sẽ xem xét một chương trình bộ đếm đơn giản thực hiện hai chỉ thị:
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:
- Nhập các dependency cần thiết từ
solana_program
- Định nghĩa điểm vào chương trình bằ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 đến 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 đến 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.
Xác định 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 xác định 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 xác định 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
để xác định trạng thái chương trình.
Struct này sẽ được sử dụng trong cả hai lệnh 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,}
Xác định các lệnh
Các lệnh đề 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 xác định 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.
Các lệnh 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 lệnh khác nhau
- Payload của biến thể đại diện cho các tham số của lệnh
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 các 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 trợ giúp. 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 bất kỳ tham số bổ sung nào 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 mã sau vào lib.rs
để định nghĩa các instruction cho chương trình bộ đếm.
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 đề cập đến 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 mã sau vào lib.rs
. Mã này sử dụng enum CounterInstruction
và phương
thức unpack
được đị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 bộ đếm
- 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
. Lệnh 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 tra hướng dẫn
Để kiểm tra các hướng dẫn chương trình, thêm các phụ thuộc 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 tra sau vào lib.rs
và chạy cargo test-sbf
để thực
thi các bài kiểm tra. 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 mẫu:
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?