У Solana кожен акаунт — це фіксована структура з п'ятьма полями, а всі користувацькі дані зберігаються у полі data як сирий масив байтів. Runtime не знає про ваші структури: він бачить лише Vec<u8>. Як саме інтерпретувати ці байти — вирішує програма-власник акаунта. Це ключова відмінність, яку треба зрозуміти до написання першої програми.

Формат даних акаунта

Кожен акаунт у мережі Solana — це кортеж із п'яти полів:

  • lamports — баланс акаунта в лампортах (1 SOL = 1 000 000 000 lamports).
  • owner — Pubkey програми, яка має право змінювати поле data.
  • data — масив байтів, у якому програма зберігає свій стан. Максимальний розмір — 10 240 байтів (10 KiB).
  • executable — прапорець: true, якщо акаунт містить скомпільовану програму (BPF bytecode).
  • rent_epoch — епоха, в якій акаунт востаннє платив за оренду (rent). Якщо акаунт має достатньо лампортів для звільнення від оренди, це поле не має практичного значення.

Важливо: Runtime перевіряє лише розмір data та права доступу через owner. Він не валідує вміст байтів і не знає, чи це JSON, Borsh чи довільний бінарний формат.

Бінарні дані (Borsh, JSON, bincode)

У екосистемі Solana стандартом де-факто став формат Borsh (Binary Object Representation Serializer for Hashing). Він детермінований, компактний і добре підходить для блокчейн-середовища, де кожен байт коштує лампортів.

Інші формати технічно можливі, але мають обмежене застосування:

  • JSON — займає значно більше місця через текстові ключі та роздільники. Може використовуватися для налагодження, але не для production.
  • bincode — підтримується в Rust, але не детермінований для деяких типів (наприклад, HashMap), що ускладнює відтворення результатів між клієнтами та валідаторами.

Рекомендація: використовуйте Borsh для всіх даних на ланцюжку. Усі серйозні бібліотеки Solana працюють із ним нативно.

Розмір даних та ліміти

Максимальний розмір data — 10 240 байтів. Цей ліміт жорстко закладений у Runtime і не може бути змінений транзакцією.

Початковий розмір акаунта визначається в момент створення. Якщо ви виділили 200 байтів, а пізніше захочете зберегти 300 — Runtime відхилить транзакцію. Щоб збільшити розмір, програма має виконати realloc (перевиділення), за яке сплачується додаткова оренда.

Мінімальний розмір — 0 байтів, але такі акаунти мають обмежене застосування (наприклад, PDA-підписанти без стану).

Серіалізація та десеріалізація

Оскільки Runtime бачить лише байти, програма сама відповідає за перетворення Rust-структур у масив байтів (серіалізацію) і назад (десеріалізацію).

Як Anchor автоматизує серіалізацію

Фреймворк Anchor генерує код серіалізації через атрибут #[account]. Під капотом він реалізує трейти BorshSerialize та BorshDeserialize, а також додає 8-байтовий дескриптор на початок даних.

Дескриптор — це перші 8 байтів SHA-256 хешу від рядка "account:<НазваСтруктури>". Він дозволяє Anchor розпізнати, який саме тип акаунта збережено за даною адресою, що корисно при десеріалізації та перевірках.

Приклад структури з Anchor:

#[account]
pub struct Note {
  pub author: Pubkey,
  pub content: String,
  pub created_at: i64,
}

Після серіалізації такий акаунт займе: 8 байтів (дескриптор) + 32 (Pubkey) + 4 (довжина рядка) + N (байти рядка) + 8 (i64). Для рядка з 50 символів це 102 байти.

Як читати дані вручну через CLI

Середовище: Devnet, Solana CLI 1.18.x.

Щоб побачити сирий вміст акаунта без десеріалізації:

solana account <ADDRESS> --url devnet

CLI виведе базову інформацію та поле data у кодуванні base58. Це корисно для перевірки того, чи акаунт взагалі існує та чи має він дані.

Для детальнішого перегляду у форматі JSON:

solana account <ADDRESS> --output json --url devnet

У відповіді ви побачите поле data як масив чисел (байтів) та поле space — виділений розмір у байтах. Щоб інтерпретувати ці байти як конкретну структуру, потрібен інструмент, який знає формат (наприклад, Anchor CLI або кастомний скрипт із @coral-xyz/anchor).

Очікуваний результат: ви бачите розмір акаунта, власника та сирий масив даних. Якщо акаунт створений через Anchor, перші 8 байтів — це дескриптор, решта — серіалізовані поля структури.

Приклад: проста структура даних

Розглянемо повний цикл: від структури в коді до запису в акаунт і читання через CLI.

Створення акаунта з даними через програму

Середовище: Devnet, Anchor 0.30.x, Rust 1.75+.

Передумови: налаштований Anchor-проєкт, SOL на Devnet-гаманці для оренди акаунтів.

Код програми (спрощено, без обробки помилок для наочності):

use anchor_lang::prelude::*;

declare_id!("YourProgramIDHere111111111111111111111");

#[account]
pub struct Note {
  pub author: Pubkey,
  pub content: String,
  pub created_at: i64,
}

#[derive(Accounts)]
pub struct CreateNote<'info> {
  #[account(init, payer = author, space = 8 + 32 + 4 + 50 + 8)]
  pub note: Account<'info, Note>,
  #[account(mut)]
  pub author: Signer<'info>,
  pub system_program: Program<'info, System>,
}

pub fn create_note(ctx: Context<CreateNote>, content: String) -> Result<()> {
  let note = &mut ctx.accounts.note;
  note.author = ctx.accounts.author.key();
  note.content = content;
  note.created_at = Clock::get()?.unix_timestamp;
  Ok(())
}

Зверніть увагу на параметр space: 8 (дескриптор Anchor) + 32 (Pubkey) + 4 (префікс довжини рядка в Borsh) + 50 (максимальна довжина контенту) + 8 (i64). Разом — 102 байти. Цей розмір фіксується при створенні акаунта.

Після розгортання та виклику інструкції акаунт note буде створено на Devnet із записаними даними.

Читання та інтерпретація даних

Після виклику інструкції отримайте адресу акаунта з логів Anchor або з клієнтського коду. Потім:

solana account <NOTE_ADDRESS> --output json --url devnet

У полі data ви побачите масив із 102 чисел. Перші 8 — дескриптор структури Note. Наступні 32 — байти Pubkey автора. Далі 4 байти — довжина рядка (little-endian), потім самі символи рядка в UTF-8, і останні 8 байтів — timestamp у little-endian.

Спосіб перевірки: порівняйте байти з 9-го по 40-й із Pubkey автора (можна отримати через solana address). Якщо вони збігаються — серіалізація працює коректно.

Відмінність від Ethereum storage

Розробники, які переходять з Ethereum, часто очікують, що стан програми зберігається «всередині» контракту. У Solana це не так.

Чому Solana використовує акаунти замість state tree

У Ethereum кожен контракт має власне сховище — ключ-значення map, де ключ і значення — 32-байтові слоти. Сховище організоване як Patricia Merkle Trie, і кожна зміна стану змінює корінь дерева, що стає частиною стану блокчейну.

Solana використовує модель account-based: стан не прив'язаний до адреси програми. Програма — це лише виконуваний код (executable account). Дані живуть у окремих акаунтах, які належать цій програмі (поле owner вказує на програму). Одна програма може мати тисячі акаунтів-даних, кожен зі своїм станом.

Переваги та компроміси

Аспект Solana (accounts) Ethereum (storage)
Паралелізм Різні акаунти можна читати/писати паралельно, якщо немає перетину Усі зміни стану контракту послідовні в межі транзакції
Гранулярність оплати Плачує лише за ті акаунти, які використовує Плачує за кожен змінений слот storage (gas)
Максимальний розмір 10 KiB на акаунт (безліч акаунтів на програму) 2^256 слотів по 32 байти (теоретично необмежено, практично обмежено gas)
Структурування даних Проєктує розробник: будь-який бінарний формат у межах 10 KiB Фіксовані 32-байтові слоти, складні типи збираються вручну
Видалення стану Акаунт можна закрити та повернути оренду Storage можна очистити (SELFDESTRUCT раніше, зараз обмежено EIP-6780)

Основний компроміс Solana: розробник сам управляє макетом пам'яті, розміром акаунтів та їхнім життєвим циклом. Це дає гнучкість і продуктивність, але вимагає явного планування схеми даних.

Типові помилки

Невірна серіалізація даних

Найчастіша помилка — серіалізувати дані одним форматом, а десеріалізувати іншим. Наприклад, записати через Borsh, а читати як JSON або навпаки. Runtime не попередить: він просто передасть байти, і ви отримаєте або сміття, або помилку парсингу.

Інший варіант — ігнорування 8-байтового дескриптора Anchor. Якщо ви читаєте Anchor-акаунт інструментом, який не знає про дескриптор (наприклад, власним скриптом із прямим викликом borsh.deserialize), усі поля змістяться на 8 байтів і дані будуть некоректними.

Спосіб перевірки: завжди серіалізуйте та десеріалізуйте однією й тією ж бібліотекою. Якщо використовуєте Anchor — читайте дані через Anchor. Якщо пишете без Anchor — не додавайте дескриптор і використовуйте borsh безпосередньо.

Перевищення ліміту розміру акаунта

Помилка виникає в двох сценаріях:

  1. Занижений space при створенні. Якщо ви виділили 50 байтів, а серіалізована структура займає 60 — транзакція впаде з помилкою invalid account data ще на етапі запису.
  2. Динамічне зростання даних без realloc. Наприклад, ви зберігаєте список елементів, і його довжина перевищила виділений простір. Без явного перерозподілу пам'яті запис не відбудеться.

Безпечний підхід: при розрахунку space завжди додавайте запас або використовуйте realloc з перевіркою ліміту в 10 KiB. Пам'ятайте, що realloc вимагає додаткової оплати оренди за нові байти та може зменшити розмір (але не нижче за поточну зайняту область).

Спосіб перевірки: перед деплоєм порахуйте розмір серіалізованої структури вручну (як у прикладі вище) і порівняйте із значенням space. Для Anchor-структур можна використати Note::INIT_SPACE (починаючи з Anchor 0.29), який автоматично рахує розмір без дескриптора — не забудьте додати 8.

Джерела