Цей розділ зосереджений на інженерних прийомах, які дозволяють довести Solana-програму до production: знизити споживання compute units, зменшити розмір облікових записів, прискорити підтвердження транзакцій і побудувати масштабовані архітектури без перевищення бюджетів та лімітів мережі. Кожен блок містить конкретні кроки, діагностику та план відкату.
Як виправити compute limit exceeded
Помилка ComputeBudgetExceeded означає, що транзакція вичерпала виділений бюджет обчислень до завершення всіх інструкцій. Типова причина — неоптимізований цикл, надмірна десеріалізація або глибоко вкладені CPI-виклики.
Діагностика: скільки CU споживає кожна інструкція
Перший крок — локалізувати інструкцію, що споживає найбільше. У локальному валідаторі запустіть програму з прапорцем --log-compute-units або додайте логування sol_log_compute_units() між інструкціями. У production використовуйте RPC-метод getTransaction з параметром commitment: confirmed та аналізуйте поле meta.computeUnitsConsumed для кожної inner instruction.
Зверніть увагу: якщо одна інструкція споживає понад 80% бюджету, саме вона є кандидатом на оптимізацію. Якщо споживання розподілене рівномірно — проблема в загальній кількості інструкцій у транзакції.
Збільшення compute budget через computeBudget
Якщо оптимізація неможлива в поточному циклі випуску, тимчасово збільште бюджет. Додайте інструкцію ComputeBudgetInstruction::set_compute_unit_limit(limit) на початок транзакції. Максимальний ліміт, який можна запросити, змінюється мережею — перевірте актуальне значення в документації Solana, оскільки воно оновлювалося кілька разів. Базовий ліміт становить 200 000 CU.
Ризик: збільшення ліміту без відповідного підвищення priority fee знижує пріоритет транзакції в черзі лідера, оскільки лідер оцінює відношення fee/CU.
Оптимізація гарячих шляхів
Гарячий шлях — це код, що виконується на кожній транзакції. Типові прийоми: заміна Vec на фіксовані масиви, якщо розмір відомий заздалегідь; винесення незмінних обчислень у константи; уникнення алокацій у циклах через попереднє резервування буфера. У Rust це означає перевагу arrayvec::ArrayVec замість Vec у критичних місцях.
План відкату при неможливості оптимізації
Якщо обчислення фундаментально потребує багато CU (наприклад, криптографічні перевірки великих наборів даних), розгляньте розбиття логіки на кілька транзакцій з проміжним станом у окремому обліковому записі. Альтернатива — перенесення важкої логіки в off-chain процесор із верифікацією результату на ланцюзі (патерн optimistic rollup на рівні програми).
Як оптимізувати compute units у програмі
Зменшення алокацій та копіювань пам'яті
Кожен виклик alloc::alloc у BPF-середовищі коштує compute units. Профілюйте алокації через sol_log_64 з лічильниками навколо підозрілих ділянок. Замініть .to_owned() та .clone() на посилання там, де Borsh-десеріалізація дозволяє працювати з borrowed даними. Для серіалізації використовуйте borsh::to_writer з попередньо алокованим буфером замість borsh::to_vec.
Оптимізація циклів та обчислень
Уникайте вкладених циклів із лінійною складністю всередині інструкції. Якщо потрібно обробити N елементів, а N може бути великим, обмежте розмір пакета на рівні клієнта або розбийте обробку на кілька транзакцій. Для пошуку в відсортованих масивах використовуйте бінарний пошук замість лінійного.
Використання syscalls замість дорогих операцій
Solana надає syscalls для типових операцій, які реалізовані нативно і коштують менше CU, ніж еквівалент на Rust. Наприклад, sol_memcpy_ серія syscall-ів ефективніша за ручне копіювання в циклі. Перевірте актуальний список syscall-ів у репозиторії Solana SDK — він розширюється.
Бенчмаркінг до та після оптимізації
Використовуйте crate solana-program-test із вимірюванням CU до та після кожної зміни. Фіксуйте результати: якщо зміна зменшила CU на менше ніж 5% при суттєвому ускладненні коду — відкочуйте. Оптимізація має бути виправданою з точки зору підтримуваності.
Як зменшити розмір облікового запису в Solana
Аналіз поточного використання простору
Викличте getAccountInfo через RPC і перевірте поле data.len(). Порівняйте із розміром, який очікує ваша програма. Якщо різниця становить понад 10%, є сенс проводити оптимізацію. Для детальнішого аналізу десеріалізуйте дані і виміряйте розмір кожного поля окремо через std::mem::size_of та фактичний розмір після Borsh-серіалізації.
Оптимізація структур даних
Замініть String на фіксовані масиви [u8; N], якщо довжина обмежена і відома. Замініть Vec<T> на [T; N], якщо максимальна кількість елементів фіксована. Використовуйте u8 замість u64 для лічильників, що не перевищують 255. Упаковуйте прапорці в бітові поля замість окремих bool.
Компресія повторюваних полів
Якщо обліковий запис містить багато однакових або спільних префіксів (наприклад, адреси з однаковою програмою), розгляньте кастомне кодування: зберігайте лише відмінну частину або індекс у словнику. Трейдофф — додаткові CU на кодування/декодування.
Міграція даних при зміні схеми
Зменшення розміру облікового запису вимагає створення нового акаунта з меншим space, копіювання даних у нову схему та закриття старого. Реалізуйте це як окрему інструкцію з перевіркою авторизації. Збережіть старий акаунт як маркер міграції на випадок відкату: якщо нова версія програми має баг, старі дані можна відновити.
Як налаштувати priority fees для швидкого підтвердження
Як працює priority fee у Solana
Priority fee — це додаткова плата за compute unit, яку лідер використовує для сортування транзакцій у черзі. Базова комісія (base fee) спалюється, а priority fee йде лідеру як винагорода. Транзакція без priority fee має найнижчий пріоритет і може застрягти в черзі за умови конгестії.
Визначення оптимальної величини fee
Оптимальне значення — мінімальне, яке забезпечує підтвердження в прийнятний час. Використовуйте RPC-метод getRecentPrioritizationFees для вибірки акаунтів, які ваша транзакція записує. Медіана або 75-й перцентиль останніх 150 слотів — це розумна отправна точка. Не використовуйте середнє значення, оскільки воно спотворюється викидами.
Динамічне регулювання fee залежно від конгестії
Реалізуйте на клієнті адаптивний алгоритм: почніть з розрахованої медіани, якщо транзакція не підтверджена протягом N слотів — збільште fee на коефіцієнт (наприклад, 1.5x) і відправте повторно з тим самим blockhash (поки він дійсний). Зупиніться після M спроб і поверніть помилку користувачеві. Це запобігає безконтрольному спалюванню SOL.
Моніторинг ефективності priority fees
Фіксуйте для кожної транзакції: запропонований compute unit price, фактичний слот підтвердження та різницю в слотах між відправкою та підтвердженням. Агрегуйте ці дані за годинами та аналізуйте, чи є кореляція між fee та швидкістю. Якщо збільшення fee не зменшує затримку — проблема не в пріоритеті, а в конфліктах записів або недоступності лідера.
Як використовувати транзакційні патерни для масштабування
Пакетна обробка транзакцій
Замість окремої транзакції на кожну дію, групуйте кілька дій в одну транзакцію, якщо вони не мають конфліктів за обліковими записами. Обмеження: розмір транзакції (1232 байти без lookup tables), кількість підписів (максимум на одну транзакцію — перевірте актуальне значення в документації) та загальний compute budget.
Паралельні транзакції без конфліктів акаунтів
Solana обробляє транзакції паралельно, якщо вони не записують в одні й ті ж облікові записи. Проєктуйте архітектуру так, щоб кожна транзакція користувача записувала лише свій PDA-акаунт, а спільний стан оновлювався окремим агрегатором. Це дозволяє лідеру виконувати сотні транзакцій одночасно.
Патерн lookup table для зменшення розміру
Address Lookup Table (ALT) дозволяє замінити повні 32-байтові адреси на 1-байтові індекси, що суттєво зменшує розмір транзакції з багатьма акаунтами. Детальніше — у відповідному розділі нижче.
Обмеження масштабування через slot time
Слот у Solana триває фіксований час (перевірте актуальне значення в документації, оскільки воно залежить від конфігурації кластера). Навіть ідеально паралельні транзакції обмежені кількістю слотів, які лідер може обробити. Це фундаментальне обмеження — його не можна обійнути на рівні програми, лише оптимізуючи розмір транзакцій та зменшуючи конфлікти.
Як стиснути дані в облікових записах
Методи стиснення: borsh, custom encoding
Borsh — це фіксований формат серіалізації, який не стискає дані, але гарантує детермінований розмір. Для справжнього стиснення використовуйте кастомне кодування: variable-length integer (varint) замість фіксованих u64 для малих значень, бітові поля для прапорців, delta-кодування для відсортованих послідовностей. Zlib чи LZ4 у BPF-середовищі можливі, але дорогі за CU — застосовуйте лише для даних, які читаються рідко.
Трейдофи між стисненням та compute cost
Кожна операція декодування коштує CU. Якщо дані читаються та записуються на кожній транзакції, складне стиснення може збільшити загальний CU більше, ніж економія на розмірі акаунта. Правило: стискуйте те, що читається рідко (історичні логи, архівні дані), і не стискуйте те, що читається на кожному виклику (поточний стан).
Вибір оптимального формату для типу даних
Цілі числа з відомим діапазоном — varint. Прапорці — бітове поле в одному байті. Фіксовані переліки — u8 з match. Рядки з обмеженим алфавітом — кастомне кодування (наприклад, base62 замість UTF-8). Адреси, що повторюються — індекси у словнику.
Міграція існуючих даних до нового формату
Реалізуйте двофазну міграцію: перша фаза — програма розуміє обидва формати і визначає, який використано (наприклад, за версійним байтом на початку). Друга фаза — окрема інструкція переписує дані у новий формат. Не робіть міграцію примусовою в існуючих інструкціях — це збільшить їхній CU без потреби для вже мігрованих акаунтів.
Як працювати з CPI та його вплив на compute budget
Вартість CPI-викликів
Кожен виклик Cross-Program Invocation має базову вартість у CU: сам виклик, передача даних, перевірка підписів. Ця вартість додається до бюджету поточної транзакції. Навіть порожній CPI-виклик коштує тисячі CU — не робіть CPI без потреби.
Обмеження глибини вкладених CPI
Максимальна глибина вкладених CPI-викликів обмежена мережею (перевірте актуальне значення в документації — історично це 4 рівні). При досягненні ліміту транзакція завершується помилкою CallDepthExceeded. Проєктуйте архітектуру з плоскою структурою викликів: якщо потрібна глибина понад 2 рівні, перегляньте дизайн.
Оптимізація даних, що передаються через CPI
Кожен байт, переданий через CPI, копіюється і коштує CU. Передавайте лише необхідні дані. Замість передачі всього облікового запису через AccountInfo, якщо програмі-цілі потрібен лише фрагмент — розгляньте передачу посилання на конкретний зсув та довжину через інструкційні дані (якщо програма-ціль це підтримує).
Діагностика перевищення бюджету через CPI
Якщо ComputeBudgetExceeded виникає всередині CPI, стек викликів у логах покаже, на якому рівні сталася помилка. Перевірте: чи не передаєте ви зайві дані, чи не викликаєте CPI у циклі, чи не робите зайву десеріалізацію в програмі-цілі. Іноді проблема не у вашій програмі, а в програмі-цілі — у такому разі зверніться до її документації щодо CU-профілю.
Як оптимізувати серіалізацію в Anchor-програмах
Anchor serialize/deserialize під капотом
Anchor автоматично генерує реалізації BorshSerialize та BorshDeserialize для структур з атрибутом #[account]. Це зручно, але генерований код не завжди оптимальний: він серіалізує всі поля послідовно, зокрема ті, які можуть не змінюватися. Для критичних шляхів розгляньте ручну серіалізацію.
Кастомні типи з оптимальною серіалізацією
Реалізуйте BorshSerialize та BorshDeserialize вручну для типів, що часто серіалізуються. Наприклад, якщо структура містить великий масив, який зазвичай порожній, серіалізуйте лише лічильник ненульових елементів та самі елементи. Це зменшує розмір запису та CU на серіалізацію.
Уникнення непотрібних десеріалізацій
У Anchor кожен обліковий запис у контексті десеріалізується автоматично при вході в інструкцію. Якщо вам потрібен лише один акаунт із п'яти, а інші потрібні лише для перевірки власника — використовуйте UncheckedAccount з ручною перевіркою замість повноцінного типу акаунта. Це економить CU на десеріалізації непотрібних даних.
Бенчмаркінг серіалізації
Виміряйте CU окремо на серіалізацію та десеріалізацію кожного типу акаунта через ізольовані інструкції-обгортки. Порівняйте з ручною реалізацією. Фіксуйте результати у таблиці: тип акаунта, розмір даних, CU (авто), CU (ручна), економія у відсотках. Це дозволить приймати обґрунтовані рішення, де ручна серіалізація виправдана.
Як використовувати lookup tables для зменшення розміру транзакції
Створення та управління address lookup table
Address Lookup Table (ALT) — це окремий обліковий запис, що зберігає масив адрес. Створюється через інструкцію CreateLookupTable з указанням адреси лідера (для визначення слота активації) та початкового слота. Після створення таблицю потрібно активувати — вона стає доступною для використання після наступного слота після вказаного.
Додавання акаунтів до таблиці
Використовуйте інструкцію ExtendLookupTable для додавання адрес. Одна таблиця може містити обмежену кількість адрес (перевірте актуальне значення в документації — історично 256). Адреси можна додавати частинами, але кожне розширення — це окрема транзакція з відповідною комісією.
Обмеження lookup tables
Таблиця прив'язана до конкретного слота активації і не може бути використана до нього. Таблицю можна закрити (деактивувати), але це незворотна операція. Адреси в таблиці не можна видалити окремо — лише деактивувати всю таблицю. Плануйте склад таблиць заздалегідь: групуйте акаунти за логічними сценаріями використання.
Практичний приклад зменшення розміру транзакції
Транзакція з 20 акаунтами без ALT: 20 × 32 байти = 640 байтів лише на адреси. З ALT: 1 × 32 байти (адреса таблиці) + 20 × 1 байт (індекси) = 52 байти. Економія — 588 байтів, що може бути критичним для вміщення в ліміт 1232 байти. На клієнті використовуйте AddressLookupTableAccount у VersionedTransaction — звичайні Transaction не підтримують ALT.
Як обробляти concurrent writes до облікових записів
Природа конфліктів записів у Solana
Дві транзакції конфліктують, якщо обидві записують (мають writable) в один і той самий обліковий запис. Лідер не може обробити їх паралельно і ставить одну в чергу після іншої. Це не помилка — транзакції будуть оброблені послідовно, але затримка зростає лінійно з кількістю конфліктів.
Патерни чергування транзакцій
Якщо кілька користувачів повинні оновлювати спільний стан, не робіть це безпосередньо. Замість цього: кожен користувач записує у свій PDA-акаунт, а окремий процес (crank) періодично агрегує зміни в спільний акаунт. Це перетворює конфлікти записів на паралельні операції з послідовною агрегацією.
Використання PDA для серіалізації доступу
Створюйте ієрархію PDA: [seed, user_pubkey] для індивідуального стану та [seed, "global"] для агрегованого. Інструкції, що працюють з індивідуальним станом, не конфліктують між собою. Інструкція агрегації конфліктує сама з собою, але викликається рідше.
Моніторинг частоти конфліктів
Аналізуйте логи валідатора або використовуйте інструменти моніторингу для виявлення акаунтів із високою частотою конфліктів. Метрика: кількість транзакцій, що записують в акаунт, поділена на кількість слотів. Якщо значення перевищує 1 — є конфлікти. Якщо перевищує 10 — критична проблема, що потребує перегляду архітектури.
Як налаштувати compute budget для складних транзакцій
Розрахунок необхідного бюджету
Не вгадуйте бюджет — вимірюйте. Запустіть транзакцію в локальному валідаторі з максимальним лімітом і зафіксуйте фактичне споживання з логів. Додайте запас 10–20% для компенсації варіативності (різні версії програми на різних валідаторах можуть давати відмінності в кілька відсотків). Не додавайте запас понад 30% — це марно збільшує вартість транзакції.
Використання computeBudget instructions
Додайте ComputeBudgetInstruction::set_compute_unit_limit як першу інструкцію в транзакції. Друга інструкція — set_compute_unit_price для встановлення priority fee. Порядок важливий: бюджетні інструкції мають йти перед інструкціями програми.
Тестування бюджету в local validator
Запустіть локальний валідатор з прапорцем --compute-unit-limit, встановленим на значення, близьке до вашого розрахованого бюджету. Якщо транзакція проходить локально з запасом 5%, але падає в devnet — різниця ймовірно пов'язана з різницею версій програми. Перевірте, що деплоїте той самий бінарник.
Моніторження реального споживання в production
Підписуйтесь на WebSocket-підключення до валідатора або використовуйте сторонні інструменти для агрегації метрик computeUnitsConsumed по ваших програмах. Встановіть алерти: якщо споживання наближається до встановленого ліміту (наприклад, понад 90%) — це сигнал для перегляду бюджету або оптимізації.
Як працювати з rent-exempt мінімумами ефективно
Формула rent-exempt мінімуму
Rent-exempt мінімум — це кількість SOL, яку потрібно депонувати на обліковому записі, щоб він ніколи не спалював rent. Формула залежить від розміру даних акаунта та параметрів rent sysvar. Оскільки ці параметри можуть змінюватися оновленнями мережі, завжди обчислюйте rent-exempt мінімум програмно через rent.minimum_balance(size), а не хардкодьте значення.
Оптимізація розміру акаунтів для зменшення витрат
Rent-exempt мінімум лінійно залежить від розміру. Зменшення акаунта з 1000 до 800 байтів економить 20% на rent. Комбінуйте це з техніками з розділу про зменшення розміру облікового запису. Для акаунтів, що створюються масово (наприклад, індивідуальні PDA кожного користувача), навіться 100 байтів економії на акаунті дає суттєвий ефект при тисячах користувачів.
Перевірка rent-exempt статусу
Перед критичними операціями перевіряйте, що акаунт залишається rent-exempt. Використовуйте account.lamports() та rent.minimum_balance(account.data_len()). Якщо lamports < minimum_balance — акаунт втратив rent-exempt статус і може бути видалений мережею. Це граничний стан, який має оброблятися окремо.
Що відбувається при втраті rent-exempt статусу
Якщо баланс акаунта падає нижче rent-exempt мінімуму (наприклад, через зняття SOL через CPI), акаунт не видаляється миттєво. Він стає вразливим до збирання сміття: будь-яка транзакція може викликати rent::sysvar::with_rent і система може видалити акаунт. Це означає втрату даних без можливості відновлення. Ніколи не дозволяйте акаунтам вашої програми втрачати rent-exempt статус — додавайте перевірки в кожну інструкцію, що змінює баланс.