Щоб писати програми для Solana, вам не потрібно знати весь Rust. Потрібен конкретний набір конструкцій, які безпосередньо використовуються в смарт-контрактах (на Solana їх називають програмами) під час компіляції в BPF-байткод. Цей довідник виділяє саме цей мінімум і показує, як кожна конструкція працює в контексті Solana-програми.

Чому Rust для Solana

Solana runtime: BPF та Rust

Solana виконує програми всередині віртуальної машини на базі BPF (Berkeley Packet Filter). Ця віртуальна машина має суворі обмеження: немає стандартної бібліотеки (no_std), немає динамічної памʼяті в звичному розумінні, обмежений стек і суворі ліміти на кількість інструкцій (зараз — 200 000 BPF-інструкцій на одну транзакцію).

Rust — єдина мова з офіційно підтримуваним інструментарієм для цієї цілі. Компілятор Rust генерує щільний і передбачуваний байткод, а система власностей (ownership) на етапі компіляції гарантує відсутність гонок даних і використання памʼяті після звільнення. Для runtime Solana це критично: одна помилка з памʼяттю зупиняє всю транзакцію, і клієнт втрачає комісію.

Мінімальний набір Rust, достатній для Solana

Повний Rust містить багато можливостей, які в BPF-програмах недоступні або непотрібні. Ось що реально використовується:

  • Змінні, базові типи, функції — фундамент логіки програми.
  • Pattern matching та типи Result і Option — єдиний ідіоматичний спосіб обробки помилок у Solana-програмах.
  • Власності та посилання — необхідні для безпечного читання й запису даних акаунтів.
  • Структури та enum-и — для опису даних акаунтів і помилок програми.
  • Крос-компіляція для BPF — перетворення Rust-коду у виконуваний байткод Solana.

Все інше — async/await, макроси процедурного типу, трейти з std — у програмах для Solana не застосовується.

Основи синтаксису Rust

Змінні, типи, функції

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

Середовище: локальна машина, Rust stable (перевірте актуальну версію командою rustc --version).

fn calculate_fee(lamports: u64, discount: bool) -> u64 {
    let base_fee: u64 = 5000;
    let mut final_fee = base_fee;

    if discount {
        final_fee = base_fee / 2;
    }

    final_fee
}

Тип u64 — беззнаковий 64-бітний цілий. Саме він використовується в Solana для позначення кількості lamports (1 SOL = 1 000 000 000 lamports). Функція повертає значення без крапки з комою в останньому виразі — це синтаксис повернення в Rust.

Очікуваний результат: функція компілюється без попереджень і повертає 5000 при discount == false, 2500 при discount == true.

Pattern matching та error handling (Result, Option)

Solana-програми не можуть «викликати виняток» у звичному розумінні. Вони повертають тип ProgramResult, який є псевдонімом для Result<(), ProgramError>. Успішне виконання — це Ok(()), помилка — Err(ProgramError::...).

use solana_program::program_error::ProgramError;

fn withdraw_lamports(balance: u64, amount: u64) -> Result<u64, ProgramError> {
    if amount > balance {
        return Err(ProgramError::InsufficientFunds);
    }

    Ok(balance - amount)
}

fn get_optional_owner(owner: Option<&str>) -> &str {
    match owner {
        Some(name) => name,
        None => "uninitialized",
    }
}

Тип Option використовується, коли значення може бути відсутнім. У Solana це часто зустрічається при роботі з необовʼязковими полями акаунтів. Pattern matching через match змушує обробити всі варіанти — компілятор не дозволить пропустити гілку None.

Типова помилка: ігнорувати помилку через .unwrap(). У BPF-програмі це призведе до паніки і відхилення транзакції з невизначеним кодом помилки. Завжди використовуйте ? або явний match.

Власності та посилання

Ownership, borrowing, lifetime — що потрібно знати

Rust має три ключові правила власностей:

  1. Кожне значення має єдиного власника.
  2. В один момент часу існує лише одне мутабельне посилання або кілька немутабельних.
  3. Посилання завжди повинні бути валідними (lifetime).

У Solana-програмах ви не працюєте з купою (heap) безпосередньо — дані акаунтів передаються як зрізи байтів (&[u8]), які ви десеріалізуєте. Але правила посилань застосовуються до цих зрізів повною мірою.

fn read_account_data(data: &[u8]) -> u64 {
    // Немутабельне посилання: можна читати, не можна змінювати
    if data.len() < 8 {
        return 0;
    }
    u64::from_le_bytes(data[0..8].try_into().unwrap())
}

fn write_account_data(data: &mut [u8], value: u64) {
    // Мутабельне посилання: можна змінювати
    if data.len() >= 8 {
        data[0..8].copy_from_slice(&value.to_le_bytes());
    }
}

Як це впливає на написання програм

Коли Solana передає вашій програмі список акаунтів, кожен акаунт представлений структурою AccountInfo. Вона містить посилання на дані (data), на власника (owner) та інші поля. Якщо ви хочете змінити дані акаунта, вам потрібно отримати мутабельний доступ до data. Компілятор не дозволить одночасно мати мутабельне посилання на дані одного акаунта і читати його ж через інше посилання.

Lifetime у Solana-програмах зазвичай не вимагають явного написання — компілятор виводить їх автоматично, оскільки життєвий цикл посилань на дані акаунтів обмежений часом виконання однієї транзакції. Але ви побачите їх у сигнатурах функцій бібліотеки solana-program, тому важливо розуміти синтаксис 'a.

Обмеження: не намагайтеся зберігати посилання на дані акаунтів між викликами транзакцій. Кожна транзакція — це окремий виклик програми, і всі посилання живуть лише всередині нього.

Структури та enum-и

Створення структур для даних акаунтів

Дані акаунта в Solana — це просто масив байтів. Щоб працювати з ними зручно, ви описуєте структуру і використовуєте бібліотеку borsh для серіалізації та десеріалізації. Borsh — це фіксований бінарний формат, який використовується в Solana замість JSON чи protobuf.

use borsh::{BorshDeserialize, BorshSerialize};

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
    pub count: u64,
    pub authority: [u8; 32],
}

Ключові моменти:

  • #[derive(BorshSerialize, BorshDeserialize)] — автоматично генерує код для перетворення структури в байти і назад.
  • pub перед полями — обовʼязково, якщо макроси серіалізації мають до них доступ.
  • [u8; 32] — фіксований масив для публічного ключа (Pubkey у Solana — це 32 байти).

Застереження: порядок полів у структурі визначає порядок байтів у серіалізованому вигляді. Якщо ви зміните порядок полів після розгортання програми, старі акаунти не десеріалізуються. Це не проблема для Devnet, але критично для mainnet.

Використання enum для помилок та станів

Enum у Rust — це не просто набір констант. Кожен варіант може містити дані, і ви можете реалізувати трейти для enum. У Solana enum-и використовуються насамперед для визначення кастомних помилок програми.

use solana_program::program_error::ProgramError;

#[derive(Debug)]
pub enum CounterError {
    Unauthorized,
    Overflow,
    Uninitialized,
}

impl From<CounterError> for ProgramError {
    fn from(e: CounterError) -> Self {
        ProgramError::Custom(e as u32)
    }
}

Реалізація трейту From<CounterError> for ProgramError дозволяє використовувати оператор ? у функціях, що повертають ProgramResult. Код помилки буде переданий у лог транзакції як число (Unauthorized — 0, Overflow — 1, Uninitialized — 2).

Перевірка: після розгортання програми на Devnet викличте її з навмисною помилкою і прочитайте логи транзакції. Ви побачите кастомний код помилки у полі Custom — це підтвердить, що enum працює коректно.

Крос-компіляція для BPF

Цільова платформа: solana-bpf

Solana-програми компілюються не для x86_64 чи ARM, а для спеціальної цілі — solana-bpf. Під капотом це відповідає LLVM-цілі bpfel-unknown-none (little-endian BPF без операційної системи). Це означає:

  • Немає std — лише core і alloc з обмеженнями.
  • Немає системних викликів, файлового вводу-виводу, мережі.
  • Вхідна точка програми — не main, а функція, яку реєструє Solana SDK.

Як перевірити, що код компілюється для Solana

Передумови: встановлений Rust stable, встановлений Solana CLI (перевірте: solana --version), підключений Devnet-кластер.

Створіть мінімальний проєкт без залежностей від Anchor — це дозволить побачити «чистий» Rust для Solana:

solana program init counter-program
cd counter-program

Ця команда створить стандартну структуру Cargo-проєкту з уже налаштованою ціллю solana-bpf у файлі Cargo.toml. Перевірте, що у Cargo.toml є рядок:

[lib]
crate-type = ["cdylib", "lib"]

Тип cdylib вказує компілятору створити динамічну бібліотеку у форматі BPF.

Для компіляції виконайте:

solana program build

Очікуваний результат: у терміналі зʼявиться рядок із шляхом до скомпільованого файлу .so, наприклад:

Program Id: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU
Binary path: /path/to/counter-program/target/deploy/counter_program.so

Наявність файлу .so — це пряме підтвердження, що код успішно скомпільовано для BPF. Якщо ви отримали помилку компіляції, найчастіші причини:

  • Використання std — замініть на no_std або видаліть непотрібні імпорти.
  • Використання типів, що залежать від аллокатора (наприклад, String, Vec) без підключення alloc та налаштування аллокатора Solana.
  • Залежність, яка не підтримує no_std — перевірте документацію crate.

Безпечний відкат: якщо компіляція зламалася після зміни коду, видаліть папку target (rm -rf target) і виконайте solana program build заново. Це усуває проблеми з кешем інкрементальної компіляції.

Після успішної компіляції наступний логічний крок — розгортання програми на Devnet і виклик через solana program deploy. Але перед тим варто ознайомитися з фреймворком Anchor, який автоматизує велику частину рутіни, описаної вище вручну.

Джерела