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

Программы Solana, написанные на Rust, имеют минимальные структурные требования, что обеспечивает гибкость в организации кода. Единственное требование — программа должна иметь entrypoint, которая определяет, где начинается выполнение программы.

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

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

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

Примеры можно найти в Solana Program Library.

Пример программы

Чтобы продемонстрировать, как создать нативную программу на 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?