Кожен акаунт у Solana, щоб існувати в мережі, має бути rent-exempt — тобто мати достатній баланс, щоб покрити оренду простору на визначений термін. Якщо ви створюєте акаунти програмно, розуміння формули розрахунку, способів оптимізації розміру та механізмів перевірки статусу безпосередньо впливає на витрати вашого застосунку та стабільність у production.

Формула rent-exempt мінімуму

Solana не стягує оренду періодично. Натомість runtime вимагає депонувати суму, розраховану за формулою, яка гарантує збереження акаунта протягом усього терміну exemption threshold. Формула:

rent_exempt_minimum = ceil(data_len × lamports_per_byte_year × (exemption_threshold / seconds_per_year))

Де:

  • data_len — розмір даних акаунта в байтах (без урахування 128 байт заголовка акаунта, який мережа додає автоматично).
  • lamports_per_byte_year — ціна оренди одного байта за рік у lamports. Це параметр Rent sysvar.
  • exemption_threshold — термін у секундах, на який має вистачити депозиту. Історично це два роки, але значення визначається Rent sysvar і може змінюватися рішенням мережі.
  • seconds_per_year — константа 31 536 000.

Ключовий момент: усі змінні, окрім data_len, є параметрами мережі. Тому єдиний інструмент оптимізації у розробника — зменшення розміру даних акаунта. Інші значення треба завжди зчитувати з Rent sysvar під час виконання, а не хардкодити.

Приклад зчитування параметрів у TypeScript:

Передумови: встановлений @solana/web3.js, активне з'єднання з RPC-вузлом.

const rent = await connection.getRent();
// rent.lamportsPerByteYear — ціна за байт-рік
// rent.exemptionThreshold — термін у секундах
// rent.burnPercent — відсоток спалювання при стягненні оренди

У Rust (on-program) Rent sysvar доступний через sysvar::rent::Rent і його можна передати в інструкцію як обов'язковий акаунт або отримати через invoke_signed з системною програмою.

Оптимізація розміру акаунтів для зменшення витрат

Оскільки rent-exempt мінімум лінійно залежить від data_len, кожен зайвий байт коштує. У production-застосунках з тисячами акаунтів різниця між неоптимізованим і стислим макетом може сягати значних сум.

Вирівнювання та пакування в Rust

Solana-програми пишуться на Rust, і компілятор за замовчуванням вирівнює поля структур за правилами архітектури (зазвичай 8 байтів для target-sbf). Це означає, що структура з одним u8 і одним u64 займе 16 байтів, а не 9.

Рішення — анотація #[repr(C, packed)]. Вона вимикає вирівнювання і розміщує поля послідовно. Але це створює обмеження: доступ до полів packed-структури вимагає unsafe або використання спеціальних бібліотек (наприклад, bytemuck), а указники на поля таких структур можуть бути невирівняними, що призводить до panic на деяких платформах.

Альтернатива — ручне впорядкування полів від найбільших до найменших, що мінімізує паддінг без packed.

Заміна типів даних

  • Bool — 1 байт. Якщо потрібен прапорець, використовуйте u8 з значеннями 0/1, а не bool (хоча в Rust розмір однаковий, це питання явності при серіалізації).
  • Enum — за замовчуванням займає мінімум 1 байт для дискримінанта, але з #[repr(u8)] ви гарантуєте розмір дискримінанта. Без явного вказування компілятор може обрати більший розмір.
  • Pubkey — завжди 32 байти. Якщо ви зберігаєте масив Pubkey, переконайтеся, що кожен із них дійсно потрібен.
  • Числа — якщо значення не перевищує 255, використовуйте u8 замість u64. Якщо не перевищує 65 535 — u16. Це найпростіший спосіб зекономити байти.

Розділення даних між кількома акаунтами

Якщо акаунт містить великий масив даних, який рідко використовується (наприклад, історичні записи), розгляньте винесення його в окремий PDA-акаунт. Rent-exempt мінімум для двох малих акаунтів може бути нижчим, ніж для одного великого, завдяки тому, що менший акаунт має менший data_len і, відповідно, менший поріг.

Однак це не завжди так: кожен акаунт має фіксовані накладні витрати на створення транзакції та обробку в runtime. Розділення має сенс, коли різниця в data_len між єдиним акаунтом і розділеними суттєва.

Що перевірити перед деплоєм

  1. Виведіть фактичний розмір серіалізованої структури (std::mem::size_of::<YourAccount>() для zero-copy або розмір байтового масиву після Borsh-серіалізації).
  2. Порівняйте з space, який ви передаєте при виклику create_account або init в Anchor. Якщо space більший за фактичний розмір — ви переплачуєте.
  3. Перевірте, чи не зарезервували ви додатковий простір «на майбутнє». Якщо це не обґрунтовано конкретним планом міграції, це прямі витрати.

Перевірка rent-exempt статусу

У production вам потрібен надійний спосіб перевірити, чи є акаунт rent-exempt, перш ніж виконувати з ним операції. Є два рівні перевірки: клієнтський (off-chain) і програмний (on-chain).

Off-chain перевірка через RPC

Виклик getAccountInfo повертає об'єкт акаунта, де поле lamports містить поточний баланс, а data — масив даних. Щоб визначити rent-exempt статус:

Передумови: активне з'єднання з RPC, відомий адрес акаунта.

const accountInfo = await connection.getAccountInfo(address);
const rent = await connection.getRent();
const rentExemptMinimum = rent.minimumBalance(accountInfo.data.length);
const isRentExempt = accountInfo.lamports >= rentExemptMinimum;

Метод rent.minimumBalance(dataLen) у @solana/web3.js інкапсує формулу, описану вище, і повертає готове значення.

Якщо accountInfo дорівнює null, акаунт не існує — він вже був видалений через втрату rent-exempt статусу або ніколи не створювався.

On-chain перевірка в програмі

У Rust-програмі використовуйте метод Rent::is_exempt():

let is_exempt = rent.is_exempt(account.lamports(), account.data_len());

Це критично важливо в інструкціях, які модифікують баланс акаунта (наприклад, знімають токени або переводять lamports). Якщо після вашої операції акаунт втратить rent-exempt статус, транзакція не буде відхилена автоматично — але акаунт стає вразливим до garbage collection.

Коли перевірка обов'язкова

  • Перед закриттям часткового балансу акаунта (partial withdrawal).
  • У CPI-викликах, де ваша програма керує лампортами стороннього акаунта.
  • Під час міграції даних між акаунтами з перерозподілом балансу.
  • У тестах — як інваріант після кожної операції, що змінює баланс.

Що відбувається при втраті rent-exempt статусу

Механізм оренди в Solana працює не як регулярне стягнення, а як умова існування акаунта. Коли баланс акаунта опускається нижче rent-exempt мінімуму, не відбувається миттєвого видалення. Запуск механізму залежить від двох факторів: епохи та звернення до акаунта.

Збір оренди (rent collection)

Оренда стягується в момент обробки транзакції, яка звертається до акаунта, на початку епохи (точніше — у першому слоті епохи, де акаунт фігурує). Runtime перевіряє баланс і, якщо він нижчий за поріг, спалює частину lamports пропорційно burn_percent з Rent sysvar і зараховує залишок на акаунт.

Важливий нюанс: наразі burn_percent у Rent sysvar на mainnet-beta встановлено на 0. Це означає, що фактичне стягнення оренди (спалювання lamports) вимкнено рішенням мережі. Проте rent-exempt мінімум як умова створення акаунта залишається чинним.

Що саме перевірити: перед тим як покладатися на відсутність стягнення, зчитайте поточне значення rent.burnPercent з Rent sysvar через ваш RPC-вузол. Цей параметр може бути змінений мережею.

Garbage collection акаунтів

Якщо акаунт втрачає rent-exempt статус і оренда фактично стягується (коли burn_percent не нульовий), після достатньої кількості епох стягнення баланс акаунта стає нульовим. У цей момент акаунт позначається як такий, що підлягає видаленню. Фізичне видалення відбувається при наступному зверненні до цього акаунта в будь-якій транзакції — runtime повертає null замість даних.

При burn_percent рівному 0, навіть якщо акаунт формально не є rent-exempt, його баланс не зменшується, і garbage collection не запускається. Але це поведінка, яку не варто сприймати як гарантію — це поточний стан параметра, а не архітектурна властивість.

Практичні наслідки для production

  • Дані незворотно втрачаються. Немає механізму відновлення видаленого акаунта. Немає «кошика», немає grace period після garbage collection.
  • Транзакції, що очікують на існуючий акаунт, падають. Якщо ваша інструкція вимагає акаунт через AccountInfo без mut і без перевірки is_signer, але акаунт видалено, транзакція поверне помилку AccountNotFound.
  • PDA-акаунти не мають особливого захисту. Те, що акаунт є PDA, не робить його immune до garbage collection. PDA-похідна адреса існує незалежно від того, чи існує сам акаунт за цією адресою.
  • Виконувані акаунти (програми) теж підлягають оренді. Але на практиці програми деплояться з значним перевищенням rent-exempt мінімуму, тому ризик мінімальний. Перевіряти статус програмного акаунта варто після будь-яких операцій з його балансом (що є нетиповим, але можливим через CPI).

План відкату при проблемах з орендою

Якщо ви виявили, що акаунт втратив rent-exempt статус:

  1. Негайно перевірте burn_percent у Rent sysvar. Якщо 0 — акаунт не втрачає баланс, у вас є час.
  2. Перекажіть lamports на акаунт до відновлення rent-exempt статусу. Сума: rent.minimumBalance(data_len) - current_lamports.
  3. Якщо акаунт уже видалено — відновлення можливе лише через повторну ініціалізацію з нуля. Усі дані втрачено. Якщо у вас є off-chain бекап стану (база даних, індексер), ви можете відтворити акаунт з тих самих PDA-насінів, але це буде новий акаунт з точки зору мережі.
  4. Запобіжний захист на рівні архітектури: реалізуйте off-chain моніторинг балансів критичних акаунтів (детальніше — у розділі про моніторинг) і автоматичне поповнення при наближенні до порогу.

Ефективна робота з rent-exempt мінімумами зводиться до трьох правил: зчитуйте параметри з Rent sysvar, а не хардкодьте їх; мінімізуйте data_len на рівні структур; перевіряйте статус після кожної операції, що змінює баланс. Це не оптимізація «на майбутнє» — це вимога до стабільності будь-якого Solana-застосунку в production.

Джерела