Структура програми на 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-type
Програми 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,}
Визначення переліку інструкцій
Визначимо інструкції, які наша програма може виконувати. Ми використаємо перелік (enum), де кожен варіант представляє різну інструкцію.
Додайте наступний код до 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:
- Точка входу: Визначає, де починається виконання програми та маршрутизує всі вхідні запити до відповідних обробників інструкцій
- Обробка інструкцій: Визначає інструкції та пов'язані з ними функції обробників
- Управління станом: Визначає структури даних акаунтів і керує їхнім станом в акаунтах, що належать програмі
- Cross Program Invocation (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
Запуск локального validator
Для розробки ми використовуватимемо локальний тестовий validator.
Спочатку налаштуйте 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
Тепер запустіть тестовий validator в окремому терміналі:
$solana-test-validator
Розгортання програми
Коли 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
Запустіть клієнт
Поки локальний validator все ще працює, виконайте клієнт:
$cargo run --example client
Очікуваний вивід:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
Коли локальний validator працює, ви можете переглядати транзакції на
Solana Explorer, використовуючи
підписи транзакцій з виводу. Зверніть увагу, що кластер у Solana Explorer має
бути встановлений на "Custom RPC URL", який за замовчуванням має значення
http://localhost:8899
, на якому працює solana-test-validator
.
Is this page helpful?