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を実装する簡単なカウンタープログラムを見ていきましょう:

  1. InitializeCounter:初期値で新しいアカウントを作成し初期化します。
  2. IncrementCounter:既存のアカウントに保存されている値をインクリメントします。

簡略化のため、このプログラムは単一のlib.rsファイルで実装されますが、実際には大きなプログラムを複数のファイルに分割することをお勧めします。

新しいプログラムの作成

まず、標準的なcargo initコマンドと--libフラグを使用して、新しいRustプロジェクトを作成します。

Terminal
cargo init counter_program --lib

プロジェクトディレクトリに移動します。デフォルトのsrc/lib.rsCargo.tomlファイルが表示されるはずです

Terminal
cd counter_program

次に、solana-program 依存関係を追加します。これはSolanaプログラムを構築するために必要な最小限の依存関係です。

Terminal
cargo add solana-program@1.18.26

次に、以下のスニペットを Cargo.toml に追加します。このコンフィグを含めないと、プログラムをビルドする際に target/deploy ディレクトリが生成されません。

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

あなたの Cargo.toml ファイルは以下のようになるはずです:

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 のデフォルトコードを以下のコードに置き換えてください。このスニペットは:

  1. solana_program から必要な依存関係をインポートします
  2. entrypoint! マクロを使用してプログラムエントリーポイントを定義します
  3. instructionsを適切なハンドラー関数にルーティングする process_instruction 関数を実装します
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(())
}

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プログラムでボーシュを使用するには:

  1. borsh クレートを Cargo.toml の依存関係として追加します:
Terminal
cargo add borsh
  1. 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の両方で使用されます。

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,
}

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 0
IncrementCounter, // variant 1
}

クライアントがプログラムを呼び出す際、instruction data(バイトのバッファ)を提供する必要があります:

  • 最初のバイトは、実行するinstructionバリアント(0、1など)を識別します
  • 残りのバイトには、シリアル化されたinstructionパラメータ(必要な場合)が含まれます

instruction data(バイト)を列挙型のバリアントに変換するために、ヘルパーメソッドを実装するのが一般的です。このメソッドは:

  1. 最初のバイトを分割してinstructionバリアントを取得します
  2. バリアントに応じて残りのバイトから追加パラメータを解析します
  3. 対応する列挙型バリアントを返します

例えば、CounterInstruction列挙型のunpackメソッド:

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),
}
}
}

カウンタープログラムのinstructionsを定義するために、以下のコードをlib.rsに追加してください。

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),
}
}
}

Instructionハンドラー

Instructionハンドラーとは、各instructionのビジネスロジックを含む関数のことです。ハンドラー関数の名前をprocess_<instruction_name>とするのが一般的ですが、任意の命名規則を選ぶことができます。

以下のコードをlib.rsに追加してください。このコードは、前のステップで定義したCounterInstruction列挙型とunpackメソッドを使用して、受信したinstructionsを適切なハンドラー関数にルーティングします:

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(())
}

次に、process_initialize_counter関数の実装を追加します。このinstructionハンドラーは:

  1. カウンターデータを保存するための新しいアカウントを作成し、スペースを割り当てます
  2. instructionに渡されたinitial_valueでアカウントデータを初期化します

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(())
}

次に、process_increment_counter関数の実装を追加します。この命令は既存のカウンターアカウントの値をインクリメントします。

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(())
}

instructionのテスト

プログラムinstructionsをテストするには、以下の依存関係を Cargo.tomlに追加してください。

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo add tokio --dev

次に、以下のテストモジュールをlib.rsに追加し、cargo test-sbfを実行してテストを実行します。オプションで、--nocaptureフラグを使用して出力内のprint文を確認できます。

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);
}
}
}

出力例:

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?

目次

ページを編集