Rustプログラム構造
Rustで書かれたSolanaプログラムは、コードの構成方法に柔軟性を持たせるため、構造的な要件は最小限に抑えられています。唯一の要件は、プログラムの実行が開始される場所を定義するentrypoint
を持つことです。
プログラム構造
ファイル構造に厳格なルールはありませんが、Solanaプログラムは通常、次のような共通のパターンに従います:
entrypoint.rs
:受信したinstructionsをルーティングするエントリーポイントを定義します。state.rs
:プログラムの状態(アカウントデータ)を定義します。instructions.rs
:プログラムが実行できるinstructionsを定義します。processor.rs
:各instructionのビジネスロジックを実装するinstruction処理関数を定義します。error.rs
:プログラムが返すことができるカスタムエラーを定義します。
例として、トークンプログラムをご覧ください。
サンプルプログラム
複数のinstructionsを持つネイティブRustプログラムの構築方法を示すために、2つのinstructionsを実装する簡単なカウンタープログラムを見ていきましょう:
InitializeCounter
:初期値で新しいアカウントを作成し初期化します。IncrementCounter
:既存のアカウントに格納されている値をインクリメントします。
簡略化のため、このプログラムは単一のlib.rs
ファイルで実装されますが、実際には大きなプログラムを複数のファイルに分割することをお勧めします。
パート1:プログラムの作成
まずはカウンタープログラムの構築から始めましょう。開始値でカウンターを初期化し、それをインクリメントできるプログラムを作成します。
新しいプログラムを作成する
まず、Solanaプログラム用の新しいRustプロジェクトを作成しましょう。
$cargo new counter_program --lib$cd counter_program
デフォルトのsrc/lib.rs
とCargo.toml
ファイルが表示されるはずです。
Cargo.toml
のedition
フィールドを2021に更新してください。そうしないと、プログラムをビルドする際にエラーが発生する可能性があります。
依存関係の追加
次に、Solanaプログラムを構築するために必要な依存関係を追加しましょう。コアSDK用のsolana-program
とシリアライゼーション用のborsh
が必要です。
$cargo add solana-program@2.2.0$cargo add borsh
Borshを使用する必要はありません。ただし、Solanaプログラムでは一般的に使用されるシリアライゼーションライブラリです。
crate-typeの設定
Solanaプログラムは動的ライブラリとしてコンパイルする必要があります。Cargoがプログラムをどのようにビルドするかを設定するために[lib]
セクションを追加します。
[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,}
instructionの列挙型を定義する
プログラムが実行できるinstructionsを定義しましょう。各バリアントが異なるinstructionを表す列挙型を使用します。
以下のコードをlib.rs
に追加してください:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
instructionのデシリアライズを実装する
次に、instruction_data
(生のバイト)をCounterInstruction
列挙型のバリアントのいずれかにデシリアライズする必要があります。Borshのtry_from_slice
メソッドがこの変換を自動的に処理します。
Borshデシリアライゼーションを使用するためにprocess_instruction
関数を更新してください:
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(())}
instructionsをハンドラーにルーティングする
次に、メインのprocess_instruction
関数を更新して、instructionsを適切なハンドラー関数にルーティングしましょう。
このルーティングパターンはSolanaプログラムでは一般的です。instruction_data
はinstructionを表す列挙型のバリアントにデシリアライズされ、その後適切なハンドラー関数が呼び出されます。各ハンドラー関数にはそのinstructionの実装が含まれています。
以下のコードをlib.rs
に追加して、process_instruction
関数を更新し、InitializeCounter
とIncrementCounter
instructionsのハンドラーを追加してください:
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(())}
初期化ハンドラーを実装する
新しいカウンターアカウントを作成して初期化するハンドラーを実装しましょう。Solanaではシステムプログラムだけがアカウントを作成できるため、Cross Program Invocation(CPI)を使用します。これは本質的に私たちのプログラムから別のプログラムを呼び出すことです。
私たちのプログラムはCPIを使用してシステムプログラムのcreate_account
instructionを呼び出します。新しいアカウントは私たちのプログラムを所有者として作成され、プログラムにアカウントへの書き込みとデータの初期化能力を与えます。
以下のコードを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プログラムを構築しました:
- エントリーポイント:プログラム実行が開始される場所を定義し、すべての着信リクエストを適切なinstruction handlerにルーティングします
- Instruction処理:instructionsとそれに関連するハンドラー関数を定義します
- 状態管理:アカウントデータ構造を定義し、プログラム所有アカウントでその状態を管理します
- Cross Program Invocation (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
Cargo.toml
のedition
が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
ファイルを生成する必要があります。テストはコンパイルされたプログラムを読み込みます。
初期化instructionのテスト
初期化instructionをテストするために、初期値を持つ新しいカウンターアカウントを作成しましょう。
以下のコードを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);
インクリメントinstructionのテスト
次に、インクリメントinstructionをテストして、カウンター値が正しく更新されることを確認しましょう。
以下のコードを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/
に2つの重要なファイルを生成します:
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
プログラムIDを使用してsolana program show
コマンドでデプロイを確認できます:
$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のクラスターは「Custom RPC
URL」に設定する必要があり、デフォルトではhttp://localhost:8899
(solana-test-validator
が実行されているアドレス)に設定されています。
Is this page helpful?