Rust 程序结构
用 Rust 编写的 Solana 程序对结构的要求极少,允许灵活地组织代码。唯一的要求是程序必须有一个
entrypoint
,它定义了程序执行的起点。
程序结构
虽然对文件结构没有严格的规定,但 Solana 程序通常遵循一个通用模式:
entrypoint.rs
:定义路由传入指令的入口点。state.rs
:定义程序状态(账户数据)。instructions.rs
:定义程序可以执行的指令。processor.rs
:定义实现每个指令业务逻辑的指令处理程序(函数)。error.rs
:定义程序可以返回的自定义错误。
例如,请参阅 Token Program。
示例程序
为了演示如何构建具有多个指令的原生 Rust 程序,我们将通过一个简单的计数器程序来实现两个指令:
InitializeCounter
:创建并初始化一个具有初始值的新账户。IncrementCounter
:增加存储在现有账户中的值。
为了简化起见,该程序将实现为单个 lib.rs
文件,尽管在实际操作中,您可能希望将较大的程序拆分为多个文件。
第 1 部分:编写程序
让我们从构建计数器程序开始。我们将创建一个程序,可以用初始值初始化计数器并对其进行递增。
创建一个新程序
首先,让我们为 Solana 程序创建一个新的 Rust 项目。
$cargo new counter_program --lib$cd counter_program
您应该会看到默认的 src/lib.rs
和 Cargo.toml
文件。
更新 edition
字段在 Cargo.toml
文件中为
2021。否则,在构建程序时可能会遇到错误。
添加依赖项
现在让我们添加构建 Solana 程序所需的依赖项。我们需要 solana-program
用于核心 SDK,以及 borsh
用于序列化。
$cargo add solana-program@2.2.0$cargo add borsh
并没有要求必须使用 Borsh。然而,它是 Solana 程序中常用的序列化库。
配置 crate-type
Solana 程序必须编译为动态库。添加 [lib]
部分以配置 Cargo 如何构建程序。
[lib]crate-type = ["cdylib", "lib"]
如果不包含此配置,在构建程序时将不会生成 target/deploy 目录。
设置程序入口点
每个 Solana 程序都有一个入口点,即程序被调用时执行的函数。让我们从添加程序所需的导入和设置入口点开始。
将以下代码添加到 lib.rs
:
use borsh::{BorshDeserialize, BorshSerialize};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 {Ok(())}
entrypoint
宏处理 input
数据的反序列化,并将其传递到 process_instruction
函数的参数中。
一个 Solana 程序 entrypoint
具有以下函数签名。开发者可以自由实现自己的
entrypoint
函数。
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
定义程序状态
现在,让我们定义将存储在计数器账户中的数据结构。这些数据将存储在账户的 data
字段中。
将以下代码添加到 lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {pub count: u64,}
定义指令枚举
让我们定义程序可以执行的指令。我们将使用一个枚举,其中每个变体代表一个不同的指令。
将以下代码添加到 lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
实现指令反序列化
现在我们需要将 instruction_data
(原始字节)反序列化为 CounterInstruction
枚举的一个变体。Borsh 的 try_from_slice
方法会自动处理这种转换。
更新 process_instruction
函数以使用 Borsh 反序列化:
pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {let instruction = CounterInstruction::try_from_slice(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;Ok(())}
将指令路由到处理器
现在让我们更新主 process_instruction
函数,将指令路由到相应的处理器函数。
这种路由模式在 Solana 程序中很常见。instruction_data
被反序列化为表示指令的枚举变体,然后调用相应的处理器函数。每个处理器函数都包含该指令的实现。
将以下代码添加到 lib.rs
,更新 process_instruction
函数并为
InitializeCounter
和 IncrementCounter
指令添加处理器:
pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {let instruction = CounterInstruction::try_from_slice(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;match instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => {process_increment_counter(program_id, accounts)?}};Ok(())}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {Ok(())}fn process_increment_counter(program_id: &Pubkey,accounts: &[AccountInfo],) -> ProgramResult {Ok(())}
实现初始化处理器
让我们实现一个处理器来创建并初始化一个新的计数器账户。由于只有 System Program 可以在 Solana 上创建账户,我们将使用 Cross Program Invocation (CPI),即从我们的程序调用另一个程序。
我们的程序通过 CPI 调用 System Program 的 create_account
指令。新账户由我们的程序作为所有者创建,使我们的程序能够写入该账户并初始化数据。
将以下代码添加到 lib.rs
,更新 process_initialize_counter
函数:
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)?;let account_space = 8;let rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);invoke(&system_instruction::create_account(payer_account.key,counter_account.key,required_lamports,account_space as u64,program_id,),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;let counter_data = CounterAccount {count: initial_value,};let mut account_data = &mut counter_account.data.borrow_mut()[..];counter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
此指令仅用于演示目的。它不包括生产程序所需的安全性和验证检查。
实现增量处理器
现在让我们实现一个处理器,用于递增现有计数器。此指令:
- 读取账户
data
字段中的counter_account
- 将其反序列化为
CounterAccount
结构体 - 将
count
字段加 1 - 将
CounterAccount
结构体重新序列化回账户的data
字段
将以下代码添加到 lib.rs
,更新 process_increment_counter
函数:
fn process_increment_counter(program_id: &Pubkey,accounts: &[AccountInfo],) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;if counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}let mut data = counter_account.data.borrow_mut();let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;counter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;counter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
此指令仅用于演示目的。它不包括生产程序所需的安全性和验证检查。
完成的程序
恭喜!您已经构建了一个完整的 Solana 程序,展示了所有 Solana 程序共享的基本结构:
- 入口点:定义程序执行的起点,并将所有传入请求路由到相应的指令处理器
- 指令处理:定义指令及其相关的处理器函数
- 状态管理:定义账户数据结构并管理其在程序拥有账户中的状态
- 跨程序调用 (CPI):调用 System Program 创建新的程序拥有账户
下一步是测试程序以确保一切正常运行。
第 2 部分:测试程序
现在让我们测试我们的计数器程序。我们将使用 LiteSVM,一个测试框架,可以让我们在不部署到集群的情况下测试程序。
添加测试依赖项
首先,让我们添加测试所需的依赖项。我们将使用 litesvm
进行测试,以及
solana-sdk
。
$cargo add litesvm@0.6.1 --dev$cargo add solana-sdk@2.2.0 --dev
创建测试模块
现在让我们为程序添加一个测试模块。我们将从基本的框架和导入开始。
将以下代码添加到 lib.rs
,直接位于程序代码下方:
#[cfg(test)]mod test {use super::*;use litesvm::LiteSVM;use solana_sdk::{account::ReadableAccount,instruction::{AccountMeta, Instruction},message::Message,signature::{Keypair, Signer},system_program,transaction::Transaction,};#[test]fn test_counter_program() {// Test implementation will go here}}
#[cfg(test)]
属性确保此代码仅在运行测试时编译。
初始化测试环境
让我们使用 LiteSVM 设置测试环境并为支付账户提供资金。
LiteSVM 模拟了 Solana 的运行环境,使我们能够在不部署到真实集群的情况下测试程序。
将以下代码添加到 lib.rs
,并更新 test_counter_program
函数:
let mut svm = LiteSVM::new();let payer = Keypair::new();svm.airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to airdrop");
加载程序
现在我们需要构建并将程序加载到测试环境中。运行 cargo build-sbf
命令以构建程序。这将在 target/deploy
目录中生成 counter_program.so
文件。
$cargo build-sbf
edition
在 Cargo.toml
中设置为 2021
。构建完成后,我们可以加载程序。
更新 test_counter_program
函数,将程序加载到测试环境中。
let program_keypair = Keypair::new();let program_id = program_keypair.pubkey();svm.add_program_from_file(program_id,"target/deploy/counter_program.so").expect("Failed to load program");
在运行测试之前,必须运行 cargo build-sbf
以生成 .so
文件。测试会加载已编译的程序。
测试初始化指令
让我们通过创建一个具有初始值的新计数器账户来测试初始化指令。
将以下代码添加到 lib.rs
中,更新 test_counter_program
函数:
let counter_keypair = Keypair::new();let initial_value: u64 = 42;println!("Testing counter initialization...");let init_instruction_data =borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value }).expect("Failed to serialize instruction");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),],);let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));let transaction = Transaction::new(&[&payer, &counter_keypair],message,svm.latest_blockhash());let result = svm.send_transaction(transaction);assert!(result.is_ok(), "Initialize transaction should succeed");let logs = result.unwrap().logs;println!("Transaction logs:\n{:#?}", logs);
验证初始化
初始化后,让我们验证计数器账户是否已正确创建,并具有预期的值。
将以下代码添加到 lib.rs
中,更新 test_counter_program
函数:
let account = svm.get_account(&counter_keypair.pubkey()).expect("Failed to get counter account");let counter: CounterAccount = CounterAccount::try_from_slice(account.data()).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("Counter initialized successfully with value: {}", counter.count);
测试增量指令
现在让我们测试增量指令,以确保它正确更新了计数器值。
将以下代码添加到 lib.rs
中,更新 test_counter_program
函数:
println!("Testing counter increment...");let increment_instruction_data =borsh::to_vec(&CounterInstruction::IncrementCounter).expect("Failed to serialize instruction");let increment_instruction = Instruction::new_with_bytes(program_id,&increment_instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true)],);let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));let transaction = Transaction::new(&[&payer, &counter_keypair],message,svm.latest_blockhash());let result = svm.send_transaction(transaction);assert!(result.is_ok(), "Increment transaction should succeed");let logs = result.unwrap().logs;println!("Transaction logs:\n{:#?}", logs);
验证最终结果
最后,让我们通过检查更新后的计数器值来验证增量是否正确。
将以下代码添加到 lib.rs
中,更新 test_counter_program
函数:
let account = svm.get_account(&counter_keypair.pubkey()).expect("Failed to get counter account");let counter: CounterAccount = CounterAccount::try_from_slice(account.data()).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("Counter incremented successfully to: {}", counter.count);
使用以下命令运行测试。--nocapture
标志会打印测试的输出。
$cargo test -- --nocapture
预期输出:
Testing counter initialization...Transaction logs:["Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program log: Counter initialized with value: 42","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",]Counter initialized successfully with value: 42Testing counter increment...Transaction logs:["Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]","Program log: Counter incremented to: 43","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",]Counter incremented successfully to: 43
第 3 部分:调用程序
现在让我们添加一个客户端脚本来调用程序。
创建客户端示例
让我们创建一个 Rust 客户端来与我们部署的程序交互。
$mkdir examples$touch examples/client.rs
将以下配置添加到 Cargo.toml
:
[[example]]name = "client"path = "examples/client.rs"
安装客户端依赖项:
$cargo add solana-client@2.2.0 --dev$cargo add tokio --dev
实现客户端代码
现在让我们实现一个客户端,用于调用我们部署的程序。
运行以下命令,从 keypair 文件中获取您的程序 ID:
$solana address -k ./target/deploy/counter_program-keypair.json
将客户端代码添加到 examples/client.rs
,并用上一个命令的输出替换 program_id
:
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");
use solana_client::rpc_client::RpcClient;use solana_sdk::{commitment_config::CommitmentConfig,instruction::{AccountMeta, Instruction},pubkey::Pubkey,signature::{Keypair, Signer},system_program,transaction::Transaction,};use std::str::FromStr;use counter_program::CounterInstruction;#[tokio::main]async fn main() {// Replace with your actual program ID from deploymentlet program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");// Connect to local clusterlet rpc_url = String::from("http://localhost:8899");let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());// Generate a new keypair for paying feeslet payer = Keypair::new();// Request airdrop of 1 SOL for transaction feesprintln!("Requesting airdrop...");let airdrop_signature = client.request_airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to request airdrop");// Wait for airdrop confirmationloop {if client.confirm_transaction(&airdrop_signature).unwrap_or(false){break;}std::thread::sleep(std::time::Duration::from_millis(500));}println!("Airdrop confirmed");println!("\nInitializing counter...");let counter_keypair = Keypair::new();let initial_value = 100u64;// Serialize the initialize instruction datalet instruction_data = borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value }).expect("Failed to serialize instruction");let initialize_instruction = Instruction::new_with_bytes(program_id,&instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true),AccountMeta::new(payer.pubkey(), true),AccountMeta::new_readonly(system_program::id(), false),],);let mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));let blockhash = client.get_latest_blockhash().expect("Failed to get blockhash");transaction.sign(&[&payer, &counter_keypair], blockhash);match client.send_and_confirm_transaction(&transaction) {Ok(signature) => {println!("Counter initialized!");println!("Transaction: {}", signature);println!("Counter address: {}", counter_keypair.pubkey());}Err(err) => {eprintln!("Failed to initialize counter: {}", err);return;}}println!("\nIncrementing counter...");// Serialize the increment instruction datalet increment_data = borsh::to_vec(&CounterInstruction::IncrementCounter).expect("Failed to serialize instruction");let increment_instruction = Instruction::new_with_bytes(program_id,&increment_data,vec![AccountMeta::new(counter_keypair.pubkey(), true)],);let mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], blockhash);match client.send_and_confirm_transaction(&transaction) {Ok(signature) => {println!("Counter incremented!");println!("Transaction: {}", signature);}Err(err) => {eprintln!("Failed to increment counter: {}", err);}}}
第 4 部分:部署程序
现在我们已经准备好了程序和客户端,让我们构建、部署并调用该程序。
构建程序
首先,让我们构建我们的程序。
$cargo build-sbf
此命令会编译您的程序,并在 target/deploy/
中生成两个重要文件:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
您可以通过运行以下命令查看程序的 ID:
$solana address -k ./target/deploy/counter_program-keypair.json
示例输出:
HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
启动本地 validator
在开发过程中,我们将使用本地测试 validator。
首先,将 Solana CLI 配置为使用 localhost:
$solana config set -ul
示例输出:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
现在在一个单独的终端中启动测试 validator:
$solana-test-validator
部署程序
在 validator 运行的情况下,将您的程序部署到本地集群:
$solana program deploy ./target/deploy/counter_program.so
示例输出:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
您可以使用 solana program show
命令和您的程序 ID 验证部署:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
示例输出:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
运行客户端
在本地 validator 仍在运行的情况下,执行客户端:
$cargo run --example client
预期输出:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
在本地 validator 运行时,您可以使用输出的交易签名在
Solana Explorer
上查看交易。请注意,Solana Explorer 上的集群必须设置为“自定义 RPC URL”,默认值为
http://localhost:8899
,即 solana-test-validator
正在运行的地址。
Is this page helpful?