Rustプログラム構造
Rustで書かれたSolanaプログラムは、コードの構成方法に柔軟性を持たせるため、構造的な要件は最小限に抑えられています。唯一の要件は、プログラムの実行が開始される場所を定義するentrypoint
を持つことです。
プログラム構造
ファイル構造に厳格なルールはありませんが、Solanaプログラムは通常、一般的なパターンに従います:
entrypoint.rs
:受信したinstructionsをルーティングするエントリーポイントを定義します。state.rs
:プログラム固有の状態(アカウントデータ)を定義します。instructions.rs
:プログラムが実行できるinstructionsを定義します。processor.rs
:各instructionのビジネスロジックを実装するinstruction処理関数を定義します。error.rs
:プログラムが返すことができるカスタムエラーを定義します。
例はSolana Program Libraryで見つけることができます。
サンプルプログラム
複数のinstructionsを持つネイティブRustプログラムの構築方法を示すために、2つのinstructionsを実装する簡単なカウンタープログラムを見ていきましょう:
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
クレートから
entrypoint!
マクロを使用して、プログラム内のエントリーポイントを定義します。
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
lib.rs
のデフォルトコードを以下のコードに置き換えてください。このスニペットは:
solana_program
から必要な依存関係をインポートしますentrypoint!
マクロを使用してプログラムエントリーポイントを定義します- instructionsを適切なハンドラー関数にルーティングする
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プログラムが呼び出されると、エントリーポイントは入力データ(バイトとして提供される)をデシリアライズして3つの値に変換し、それらをprocess_instruction
関数に渡します:
program_id
: 呼び出されるプログラム(現在のプログラム)の公開鍵accounts
: 呼び出されるinstructionに必要なアカウントのAccountInfo
instruction_data
: 実行するinstructionとその必要な引数を指定するプログラムに渡される追加データ
これら3つのパラメータは、クライアントがプログラムを呼び出すためのinstructionを構築する際に提供しなければならないデータに直接対応しています。
プログラムの状態を定義する
Solanaプログラムを構築する際、通常はプログラムの状態(プログラムによって作成され所有されるアカウントに保存されるデータ)を定義することから始めます。
プログラムの状態は、プログラムのアカウントのデータレイアウトを表すRustの構造体を使用して定義されます。プログラムの異なるタイプのアカウントを表すために複数の構造体を定義することができます。
アカウントを扱う際には、プログラムのデータ型をアカウントのデータフィールドに保存される生のバイトとの間で変換する方法が必要です:
- シリアライゼーション:データ型をバイトに変換してアカウントのデータフィールドに保存する
- デシリアライゼーション:アカウントに保存されているバイトをデータ型に変換する
Solanaプログラム開発では任意のシリアライゼーション形式を使用できますが、Borshが一般的に使用されています。Solanaプログラムでボーシュを使用するには:
borsh
クレートを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
に追加してプログラムの状態を定義します。この構造体は初期化instructionsと増分instructionsの両方で使用されます。
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,}
Instructionsを定義する
Instructionsとは、Solanaプログラムが実行できる異なる操作を指します。これらをプログラムのパブリックAPIと考えてください - ユーザーがプログラムと対話する際に実行できるアクションを定義します。
Instructionsは通常、Rustのenumを使用して定義されます:
- 各enumバリアントは異なるinstructionを表します
- バリアントのペイロードはinstructionのパラメータを表します
Rustの列挙型のバリアントは、0から始まる番号が暗黙的に付けられることに注意してください。
以下は、2つのinstructionsを定義する列挙型の例です:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
クライアントがプログラムを呼び出す際、instruction data(バイトのバッファ)を提供する必要があります:
- 最初のバイトは、実行するinstructionバリアント(0、1など)を識別します
- 残りのバイトには、シリアル化されたinstructionパラメータ(必要な場合)が含まれます
instruction data(バイト)を列挙型のバリアントに変換するために、ヘルパーメソッドを実装するのが一般的です。このメソッドは:
- 最初のバイトを分割してinstructionバリアントを取得します
- バリアントに応じて残りのバイトから追加パラメータを解析します
- 対応する列挙型バリアントを返します
例えば、CounterInstruction
列挙型のunpack
メソッド:
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),}}}
カウンタープログラムのinstructionsを定義するために、以下のコードを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),}}}
Instructionハンドラー
Instructionハンドラーとは、各instructionのビジネスロジックを含む関数のことです。ハンドラー関数の名前をprocess_<instruction_name>
とするのが一般的ですが、任意の命名規則を選ぶことができます。
以下のコードをlib.rs
に追加してください。このコードは、前のステップで定義したCounterInstruction
列挙型とunpack
メソッドを使用して、受信したinstructionsを適切なハンドラー関数にルーティングします:
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
関数の実装を追加します。このinstructionハンドラーは:
- カウンターデータを保存するための新しいアカウントを作成し、スペースを割り当てます
- instructionに渡された
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(())}
instructionのテスト
プログラムinstructionsをテストするには、以下の依存関係を
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
フラグを使用して出力内のprint文を確認できます。
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?