Структура программы на Rust
Программы Solana, написанные на Rust, имеют минимальные структурные требования,
что обеспечивает гибкость в организации кода. Единственное требование —
программа должна иметь entrypoint
, которая определяет, где начинается
выполнение программы.
Структура программы
Хотя строгих правил для структуры файлов нет, программы Solana обычно следуют общему шаблону:
entrypoint.rs
: Определяет точку входа, которая направляет входящие инструкции.state.rs
: Определяют специфичное для программы состояние (данные аккаунта).instructions.rs
: Определяет инструкции, которые программа может выполнять.processor.rs
: Определяет обработчики инструкций (функции), реализующие бизнес-логику для каждой инструкции.error.rs
: Определяет пользовательские ошибки, которые программа может возвращать.
Примеры можно найти в Solana Program Library.
Пример программы
Чтобы продемонстрировать, как создать нативную программу на Rust с несколькими инструкциями, мы рассмотрим простую программу-счетчик, которая реализует две инструкции:
InitializeCounter
: Создает и инициализирует новый аккаунт с начальным значением.IncrementCounter
: Увеличивает значение, хранящееся в существующем аккаунте.
Для простоты программа будет реализована в одном lib.rs
файле, хотя на
практике вы можете разделить более крупные программы на несколько файлов.
Создание новой программы
Сначала создайте новый проект Rust, используя стандартную команду cargo init
с
флагом --lib
.
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 — это функция, которая вызывается при обращении к программе. Точка входа имеет следующее базовое определение, и разработчики могут свободно создавать собственную реализацию функции точки входа.
Для простоты используйте макрос
entrypoint!
из крейта solana_program
для определения точки входа в вашей программе.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Замените стандартный код в lib.rs
следующим кодом. Этот фрагмент:
- Импортирует необходимые зависимости из
solana_program
- Определяет точку входа программы с помощью макроса
entrypoint!
- Реализует функцию
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 вызывается, точка входа
десериализует
входные данные
(предоставленные в виде байтов) в три значения и передает их функции
process_instruction
:
program_id
: Публичный ключ вызываемой программы (текущей программы)accounts
:AccountInfo
для аккаунтов, необходимых для выполнения вызываемой инструкцииinstruction_data
: Дополнительные данные, передаваемые программе, которые определяют инструкцию для выполнения и её необходимые аргументы
Эти три параметра напрямую соответствуют данным, которые клиенты должны предоставить при создании инструкции для вызова программы.
Определение состояния программы
При создании программы Solana вы обычно начинаете с определения состояния вашей программы - данных, которые будут храниться в аккаунтах, созданных и принадлежащих вашей программе.
Состояние программы определяется с помощью структур Rust, которые представляют макет данных аккаунтов вашей программы. Вы можете определить несколько структур для представления различных типов аккаунтов для вашей программы.
При работе с аккаунтами вам нужен способ преобразования типов данных вашей программы в байты и обратно, которые хранятся в поле данных аккаунта:
- Сериализация: преобразование ваших типов данных в байты для хранения в поле данных аккаунта
- Десериализация: преобразование байтов, хранящихся в аккаунте, обратно в ваши типы данных
Хотя вы можете использовать любой формат сериализации для разработки программ Solana, обычно используется Borsh. Чтобы использовать Borsh в вашей программе Solana:
- Добавьте крейт
borsh
как зависимость в вашCargo.toml
:
cargo add borsh
- Импортируйте трейты 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
для определения состояния
программы. Эта структура будет использоваться как в инструкциях инициализации,
так и в инструкциях инкремента.
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,}
Определение инструкций
Инструкции относятся к различным операциям, которые может выполнять ваша программа Solana. Думайте о них как о публичных API для вашей программы - они определяют, какие действия пользователи могут выполнять при взаимодействии с вашей программой.
Инструкции обычно определяются с помощью перечисления Rust, где:
- Каждый вариант перечисления представляет собой отдельную инструкцию
- Полезная нагрузка варианта представляет параметры инструкции
Обратите внимание, что варианты перечисления Rust нумеруются автоматически, начиная с 0.
Ниже приведен пример перечисления, определяющего две инструкции:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
Когда клиент вызывает вашу программу, он должен предоставить instruction data (в виде буфера байтов), где:
- Первый байт определяет, какой вариант инструкции выполнить (0, 1 и т.д.)
- Оставшиеся байты содержат сериализованные параметры инструкции (если требуются)
Для преобразования instruction data (байтов) в вариант перечисления обычно реализуют вспомогательный метод. Этот метод:
- Отделяет первый байт для получения варианта инструкции
- Сопоставляет вариант и анализирует дополнительные параметры из оставшихся байтов
- Возвращает соответствующий вариант перечисления
Например, метод unpack
для перечисления CounterInstruction
:
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),}}}
Добавьте следующий код в 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),}}}
Обработчики инструкций
Обработчики инструкций — это функции, содержащие бизнес-логику для каждой
инструкции. Обычно функции-обработчики называют process_<instruction_name>
, но
вы можете выбрать любое соглашение об именовании.
Добавьте следующий код в lib.rs
. Этот код использует перечисление
CounterInstruction
и метод unpack
, определенные в предыдущем шаге, для
маршрутизации входящих инструкций к соответствующим функциям-обработчикам:
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
. Этот обработчик
инструкций:
- Создает и выделяет пространство для нового аккаунта для хранения данных счетчика
- Инициализирует данные аккаунта с помощью
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(())}
Тестирование инструкций
Чтобы протестировать инструкции программы, добавьте следующие зависимости в
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
для просмотра
выводимых сообщений в результатах.
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?