Структура програми на Rust

Програми Solana, написані на Rust, мають мінімальні структурні вимоги, що дозволяє гнучко організовувати код. Єдина вимога полягає в тому, що програма повинна мати entrypoint, який визначає, де починається виконання програми.

Структура програми

Хоча немає суворих правил щодо структури файлів, програми Solana зазвичай слідують загальному шаблону:

  • entrypoint.rs: Визначає точку входу, яка маршрутизує вхідні інструкції.
  • state.rs: Визначають специфічний для програми стан (дані облікового запису).
  • instructions.rs: Визначає інструкції, які програма може виконувати.
  • processor.rs: Визначає обробники інструкцій (функції), які реалізують бізнес-логіку для кожної інструкції.
  • error.rs: Визначає користувацькі помилки, які програма може повертати.

Ви можете знайти приклади в Бібліотеці програм Solana.

Приклад програми

Щоб продемонструвати, як створити нативну програму на Rust з кількома інструкціями, ми розглянемо просту програму-лічильник, яка реалізує дві інструкції:

  1. InitializeCounter: Створює та ініціалізує новий обліковий запис з початковим значенням.
  2. IncrementCounter: Збільшує значення, що зберігається в існуючому обліковому записі.

Для простоти програма буде реалізована в одному файлі lib.rs, хоча на практиці ви можете розділити більші програми на кілька файлів.

Створення нової програми

Спочатку створіть новий проект Rust, використовуючи стандартну команду cargo init з прапорцем --lib.

Terminal
cargo init counter_program --lib

Перейдіть до каталогу проекту. Ви повинні побачити типові файли src/lib.rs та Cargo.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 — це функція, яка викликається при виклику програми. Точка входу має наступне базове визначення, і розробники можуть вільно створювати власну реалізацію функції точки входу.

Для простоти використовуйте макрос entrypoint! з пакету solana_program, щоб визначити точку входу у вашій програмі.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

Замініть стандартний код у lib.rs наступним кодом. Цей фрагмент:

  1. Імпортує необхідні залежності з solana_program
  2. Визначає точку входу програми за допомогою макросу entrypoint!
  3. Реалізує функцію 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 викликається, точка входу десеріалізує вхідні дані (надані як байти) у три значення та передає їх функції process_instruction:

  • program_id: Публічний ключ програми, яка викликається (поточна програма)
  • accounts: AccountInfo для облікових записів, необхідних для інструкції, що викликається
  • instruction_data: Додаткові дані, передані програмі, які визначають інструкцію для виконання та її необхідні аргументи

Ці три параметри безпосередньо відповідають даним, які клієнти повинні надати при створенні інструкції для виклику програми.

Визначення стану програми

Під час створення програми Solana ви зазвичай почнете з визначення стану вашої програми - даних, які будуть зберігатися в облікових записах, створених і належних вашій програмі.

Стан програми визначається за допомогою структур Rust, які представляють макет даних облікових записів вашої програми. Ви можете визначити кілька структур для представлення різних типів облікових записів для вашої програми.

Під час роботи з обліковими записами вам потрібен спосіб перетворення типів даних вашої програми в байти та з байтів, що зберігаються в полі даних облікового запису:

  • Серіалізація: Перетворення ваших типів даних у байти для зберігання в полі даних облікового запису
  • Десеріалізація: Перетворення байтів, що зберігаються в обліковому записі, назад у ваші типи даних

Хоча ви можете використовувати будь-який формат серіалізації для розробки програм Solana, зазвичай використовується Borsh. Щоб використовувати Borsh у вашій програмі Solana:

  1. Додайте крейт borsh як залежність до вашого Cargo.toml:
Terminal
cargo add borsh
  1. Імпортуйте трейти Borsh і використовуйте макрос derive для реалізації трейтів для ваших структур:
use borsh::{BorshSerialize, BorshDeserialize};
// Define struct representing our counter account's data
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Додайте структуру CounterAccount до lib.rs для визначення стану програми. Ця структура буде використовуватися як в інструкціях ініціалізації, так і в інструкціях інкременту.

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

Визначення інструкцій

Інструкції відносяться до різних операцій, які може виконувати ваша програма Solana. Думайте про них як про публічні API для вашої програми - вони визначають, які дії можуть виконувати користувачі при взаємодії з вашою програмою.

Інструкції зазвичай визначаються за допомогою перерахування Rust, де:

  • Кожен варіант перерахування представляє різну інструкцію
  • Корисне навантаження варіанту представляє параметри інструкції

Зверніть увагу, що варіанти перерахування Rust нумеруються автоматично, починаючи з 0.

Нижче наведено приклад перерахування, що визначає дві інструкції:

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}

Коли клієнт викликає вашу програму, він повинен надати instruction data (як буфер байтів), де:

  • Перший байт визначає, який варіант інструкції виконати (0, 1 тощо)
  • Решта байтів містять серіалізовані параметри інструкції (якщо потрібно)

Для перетворення instruction data (байтів) у варіант перерахування, зазвичай реалізують допоміжний метод. Цей метод:

  1. Відокремлює перший байт, щоб отримати варіант інструкції
  2. Зіставляє варіант і розбирає будь-які додаткові параметри з решти байтів
  3. Повертає відповідний варіант перерахування

Наприклад, метод unpack для перерахування CounterInstruction:

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

Додайте наступний код до 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),
}
}
}

Обробники інструкцій

Обробники інструкцій — це функції, які містять бізнес-логіку для кожної інструкції. Зазвичай функції-обробники називають process_<instruction_name>, але ви можете вибрати будь-яку схему іменування.

Додайте наступний код до lib.rs. Цей код використовує перерахування CounterInstruction та метод unpack, визначені на попередньому кроці, для маршрутизації вхідних інструкцій до відповідних функцій-обробників:

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. Цей обробник інструкцій:

  1. Створює та виділяє місце для нового облікового запису для зберігання даних лічильника
  2. Ініціалізує дані облікового запису значенням 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(())
}

Тестування інструкцій

Щоб протестувати інструкції програми, додайте такі залежності до 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, щоб побачити виведення у консолі.

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?