Rust 程序结构
用 Rust 编写的 Solana 程序对结构的要求极少,允许灵活地组织代码。唯一的要求是程序必须有一个
entrypoint
,它定义了程序开始执行的位置。
程序结构
虽然对文件结构没有严格的规定,但 Solana 程序通常遵循一个通用模式:
entrypoint.rs
:定义路由传入指令的入口点。state.rs
:定义程序特定的状态(账户数据)。instructions.rs
:定义程序可以执行的指令。processor.rs
:定义指令处理程序(函数),实现每个指令的业务逻辑。error.rs
:定义程序可以返回的自定义错误。
您可以在 Solana 程序库 中找到示例。
示例程序
为了演示如何构建具有多个指令的原生 Rust 程序,我们将介绍一个简单的计数器程序,该程序实现了两个指令:
InitializeCounter
:创建并初始化一个具有初始值的新账户。IncrementCounter
:增加存储在现有账户中的值。
为简单起见,该程序将在一个 lib.rs
文件中实现,但在实际中,您可能希望将较大的程序拆分为多个文件。
创建一个新程序
首先,使用标准的 cargo init
命令和 --lib
标志创建一个新的 Rust 项目。
cargo init counter_program --lib
导航到项目目录。您应该会看到默认的 src/lib.rs
和 Cargo.toml
文件。
cd counter_program
接下来,添加 solana-program
依赖项。这是构建 Solana 程序所需的最低依赖项。
cargo add solana-program@1.18.26
接下来,将以下代码片段添加到 Cargo.toml
文件中。如果您不包含此配置,在构建程序时将不会生成 target/deploy
目录。
[lib]crate-type = ["cdylib", "lib"]
您的 Cargo.toml
文件应如下所示:
[package]name = "counter_program"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "lib"][dependencies]solana-program = "1.18.26"
程序入口点
Solana 程序入口点是程序被调用时执行的函数。入口点具有以下原始定义,开发者可以自由创建自己的入口点函数实现。
为了简化操作,请使用来自 solana_program
crate 的
entrypoint!
宏来定义程序中的入口点。
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
将 lib.rs
文件中的默认代码替换为以下代码。此代码片段:
- 从
solana_program
导入所需的依赖项 - 使用
entrypoint!
宏定义程序入口点 - 实现
process_instruction
函数,该函数将指令路由到相应的处理程序函数
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(())}
entrypoint!
宏需要一个具有以下
类型签名
的函数作为参数:
pub type ProcessInstruction =fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
当 Solana 程序被调用时,入口点会将
输入数据
(以字节形式提供)
反序列化
为三个值,并将它们传递给
process_instruction
函数:
program_id
:被调用程序(当前程序)的公钥accounts
:指令调用所需账户的AccountInfo
instruction_data
:传递给程序的附加数据,指定要执行的指令及其所需参数
这三个参数直接对应于客户端在构建调用程序的指令时必须提供的数据。
定义程序状态
在构建 Solana 程序时,通常会从定义程序的状态开始——也就是程序创建和拥有的账户中存储的数据。
程序状态是通过 Rust 结构体定义的,这些结构体表示程序账户的数据布局。您可以定义多个结构体来表示程序的不同账户类型。
在处理账户时,您需要一种方法将程序的数据类型与存储在账户数据字段中的原始字节进行相互转换:
- 序列化:将数据类型转换为字节以存储在账户的数据字段中
- 反序列化:将存储在账户中的字节转换回数据类型
虽然您可以为 Solana 程序开发使用任何序列化格式,但 Borsh 是常用的格式。要在 Solana 程序中使用 Borsh:
- 将
borsh
crate 添加为Cargo.toml
的依赖项:
cargo add borsh
- 导入 Borsh 特性,并使用派生宏为结构体实现这些特性:
use borsh::{BorshSerialize, BorshDeserialize};// Define struct representing our counter account's data#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
将 CounterAccount
结构体添加到 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 logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
定义指令
指令是指 Solana 程序可以执行的不同操作。可以将它们视为程序的公共 API——它们定义了用户在与程序交互时可以采取的操作。
指令通常使用 Rust 枚举定义,其中:
- 每个枚举变体表示一个不同的指令
- 变体的负载表示指令的参数
请注意,Rust 枚举的变体会从 0 开始隐式编号。
以下是定义两个指令的枚举示例:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
当客户端调用您的程序时,他们必须提供指令数据(作为字节缓冲区),其中:
- 第一个字节标识要执行的指令变体(0、1 等)
- 剩余的字节包含序列化的指令参数(如果需要)
为了将指令数据(字节)转换为枚举的变体,通常会实现一个辅助方法。该方法:
- 分离第一个字节以获取指令变体
- 匹配变体并从剩余字节中解析任何附加参数
- 返回相应的枚举变体
例如,unpack
方法适用于 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),}}}
将以下代码添加到 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 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),}}}
指令处理器
指令处理器是指包含每个指令业务逻辑的函数。通常将处理器函数命名为
process_<instruction_name>
,但您可以自由选择任何命名约定。
将以下代码添加到 lib.rs
中。此代码使用前一步中定义的 CounterInstruction
枚举和 unpack
方法,将传入的指令路由到相应的处理器函数:
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(())}
接下来,添加 process_initialize_counter
函数的实现。此指令处理器:
- 创建并分配空间以存储计数器数据的新账户
- 使用传递给指令的
initial_value
初始化账户数据
// 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(())}
接下来,添加 process_increment_counter
函数的实现。此指令会增加现有计数器账户的值。
// 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(())}
指令测试
要测试程序指令,请添加以下依赖项到 Cargo.toml
。
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
然后将以下测试模块添加到 lib.rs
并运行 cargo test-sbf
来执行测试。可选地,使用 --nocapture
标志以在输出中查看打印语句。
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);}}}
示例输出:
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?