Структура программы на Rust
Программы Solana, написанные на Rust, имеют минимальные требования к структуре,
что позволяет гибко организовывать код. Единственное требование — программа
должна иметь entrypoint
, который определяет, где начинается выполнение
программы.
Структура программы
Хотя строгих правил для структуры файлов нет, программы Solana обычно следуют общему шаблону:
entrypoint.rs
: Определяет точку входа, которая направляет входящие инструкции.state.rs
: Определяет состояние программы (данные аккаунта).instructions.rs
: Определяет инструкции, которые программа может выполнять.processor.rs
: Определяет обработчики инструкций (функции), которые реализуют бизнес-логику для каждой инструкции.error.rs
: Определяет пользовательские ошибки, которые программа может возвращать.
Например, см. Token Program.
Пример программы
Чтобы продемонстрировать, как создать нативную программу на Rust с несколькими инструкциями, мы рассмотрим простую программу-счётчик, которая реализует две инструкции:
InitializeCounter
: Создаёт и инициализирует новый аккаунт с начальным значением.IncrementCounter
: Увеличивает значение, хранящееся в существующем аккаунте.
Для простоты программа будет реализована в одном файле lib.rs
, хотя на
практике вы можете разделить более крупные программы на несколько файлов.
Часть 1: Написание программы
Начнём с создания программы-счётчика. Мы создадим программу, которая может инициализировать счётчик с начальным значением и увеличивать его.
Создайте новую программу
Сначала давайте создадим новый проект на Rust для нашей программы Solana.
$cargo new counter_program --lib$cd counter_program
Вы должны увидеть файлы по умолчанию src/lib.rs
и Cargo.toml
.
Обновите поле edition
в файле Cargo.toml
до 2021. В противном случае вы
можете столкнуться с ошибкой при сборке программы.
Добавьте зависимости
Теперь давайте добавим необходимые зависимости для создания программы Solana.
Нам нужны solana-program
для основного SDK и borsh
для сериализации.
$cargo add solana-program@2.2.0$cargo add borsh
Использование Borsh не является обязательным. Однако это часто используемая библиотека сериализации для программ Solana.
Настройте тип crate
Программы Solana должны компилироваться как динамические библиотеки. Добавьте
секцию [lib]
для настройки сборки программы с помощью Cargo.
[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,}
Определение перечисления инструкций
Давайте определим инструкции, которые может выполнять наша программа. Мы будем использовать перечисление, где каждый вариант представляет собой отдельную инструкцию.
Добавьте следующий код в lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
Реализация десериализации инструкций
Теперь нам нужно десериализовать instruction_data
(сырые байты) в один из
вариантов перечисления CounterInstruction
. Метод Borsh try_from_slice
автоматически выполняет эту конвертацию.
Обновите функцию process_instruction
, чтобы использовать десериализацию Borsh:
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(())}
Маршрутизация инструкций к обработчикам
Теперь давайте обновим основную функцию process_instruction
, чтобы
маршрутизировать инструкции к соответствующим функциям-обработчикам.
Этот шаблон маршрутизации является общим для программ Solana. instruction_data
десериализуется в вариант перечисления, представляющий инструкцию, после чего
вызывается соответствующая функция-обработчик. Каждая функция-обработчик
включает реализацию для этой инструкции.
Добавьте следующий код в lib.rs
, обновив функцию process_instruction
и
добавив обработчики для инструкций InitializeCounter
и IncrementCounter
:
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(())}
Реализация обработчика инициализации
Давайте реализуем обработчик для создания и инициализации нового аккаунта счетчика. Поскольку только System Program может создавать аккаунты в Solana, мы будем использовать Cross Program Invocation (CPI), по сути, вызывая другую программу из нашей программы.
Наша программа выполняет CPI для вызова инструкции create_account
из System
Program. Новый аккаунт создается с нашей программой в качестве владельца, что
дает нашей программе возможность записывать данные в аккаунт и инициализировать
их.
Добавьте следующий код в 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:
- Точка входа: Определяет, где начинается выполнение программы, и направляет все входящие запросы к соответствующим обработчикам инструкций
- Обработка инструкций: Определяет инструкции и их связанные функции-обработчики
- Управление состоянием: Определяет структуры данных аккаунтов и управляет их состоянием в аккаунтах, принадлежащих программе
- Вызов программ (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
для сборки программы. Это создаст файл
counter_program.so
в директории target/deploy
.
$cargo build-sbf
Убедитесь, что edition
в Cargo.toml
установлен на 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
. Тест загружает скомпилированную программу.
Тестирование инструкции инициализации
Давайте протестируем инструкцию инициализации, создав новый аккаунт счетчика с начальным значением.
Добавьте следующий код в 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);
Тестирование инструкции увеличения
Теперь давайте протестируем инструкцию увеличения, чтобы убедиться, что она правильно обновляет значение счетчика.
Добавьте следующий код в 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
Реализация клиентского кода
Теперь давайте реализуем клиент, который будет вызывать наше развернутое программное обеспечение.
Выполните следующую команду, чтобы получить ID программы из файла keypair:
$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/
:
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
Запустите локальный валидатор
Для разработки мы будем использовать локальный тестовый валидатор.
Сначала настройте 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
Теперь запустите тестовый валидатор в отдельном терминале:
$solana-test-validator
Разверните программу
С работающим валидатором разверните вашу программу в локальном кластере:
$solana program deploy ./target/deploy/counter_program.so
Пример вывода:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Вы можете проверить развертывание, используя команду solana program show
с
вашим ID программы:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Пример вывода:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Запустите клиент
При запущенном локальном валидаторе выполните клиент:
$cargo run --example client
Ожидаемый вывод:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
При запущенном локальном валидаторе вы можете просмотреть транзакции в
Solana Explorer, используя
подписи транзакций из вывода. Обратите внимание, что кластер в Solana Explorer
должен быть установлен на "Custom RPC URL", который по умолчанию соответствует
http://localhost:8899
, на котором работает solana-test-validator
.
Is this page helpful?