Tài liệu SolanaPhát triển chương trìnhChương trình Rust

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:

  1. InitializeCounter: Tạo và khởi tạo một tài khoản mới với giá trị ban đầu.
  2. 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.

Terminal
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.rsCargo.toml mặc định

Terminal
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.

Terminal
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.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

Tệp Cargo.toml của bạn nên trông như sau:

Cargo.toml
[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:

  1. Import các dependency cần thiết từ solana_program
  2. Định nghĩa điểm vào chương trình sử dụng macro entrypoint!
  3. 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
lib.rs
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 logic
Ok(())
}

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ọi
  • instruction_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:

  1. Thêm crate borsh như một dependency vào Cargo.toml của bạn:
Terminal
cargo add borsh
  1. 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ị.

lib.rs
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 logic
Ok(())
}
#[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 0
IncrementCounter, // 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:

  1. Tách byte đầu tiên để lấy biến thể instruction
  2. 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
  3. 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 byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let 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.

lib.rs
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 logic
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}
impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let 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:

lib.rs
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Unpack instruction data
let instruction = CounterInstruction::unpack(instruction_data)?;
// Match instruction type
match 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:

  1. 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
  2. Khởi tạo dữ liệu tài khoản với initial_value được truyền vào instruction

lib.rs
// Initialize a new counter account
fn 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 account
let account_space = 8; // Size in bytes to store a u64
// Calculate minimum balance for rent exemption
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
// Create the counter account
invoke(
&system_instruction::create_account(
payer_account.key, // Account paying for the new account
counter_account.key, // Account to be created
required_lamports, // Amount of lamports to transfer to the new account
account_space as u64, // Size in bytes to allocate for the data field
program_id, // Set program owner to our program
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
// Create a new CounterAccount struct with the initial value
let counter_data = CounterAccount {
count: initial_value,
};
// Get a mutable reference to the counter account's data
let mut account_data = &mut counter_account.data.borrow_mut()[..];
// Serialize the CounterAccount struct into the account's data
counter_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ó.

lib.rs
// Update an existing counter's value
fn 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 ownership
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Mutable borrow the account data
let mut data = counter_account.data.borrow_mut();
// Deserialize the account data into our CounterAccount struct
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
// Increment the counter value
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
// Serialize the updated counter data back into the account
counter_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.

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo 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.

Terminal
cargo test-sbf -- --nocapture

lib.rs
#[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 account
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
// Step 1: Initialize the counter
println!("Testing counter initialization...");
// Create initialization instruction
let mut init_instruction_data = vec![0]; // 0 = initialize instruction
init_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 instruction
let 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 data
let 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 counter
println!("Testing counter increment...");
// Create increment instruction
let increment_instruction = Instruction::new_with_bytes(
program_id,
&[1], // 1 = increment instruction
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
// Send transaction with increment instruction
let 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 data
let 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ụ:

Terminal
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 success
test test::test_counter_program ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Is this page helpful?

Mục lục

Chỉnh sửa trang