Складна транзакція на Solana — це не лише кілька інструкцій у пакеті. Це ланцюжок CPI-викликів, обробка великих облікових записів, ітерації над масивами даних. Усі ці операції споживають compute units (CU), і якщо бюджету не вистачить — транзакція впаде з помилкою ComputationalBudgetExceeded. Цей матеріал дає покрокову методологію: від розрахунку потрібного обсягу CU до моніторингу реального споживання в production.
Версії для прикладів у «Як налаштувати compute budget для складних транзакцій» перевірено 2 серпня 2026 року. Стабільна гілка Anchor v1 має релізи 1.0.x і орієнтується на Solana 3.x; Anchor v2 у документації позначений як alpha. Приклади для Anchor 0.29–0.32 залишаються лише відтворюваними прикладами для зафіксованого legacy-середовища: їх не слід переносити в новий проєкт без міграції залежностей і повторного тестування. Клієнт @anchor-lang/core сумісний із legacy @solana/web3.js v1, а не з v2.
Розрахунок необхідного бюджету
Передумови: ви маєте працювати з кодом програми (Rust або Anchor) і розуміти, які інструкції входять до вашої транзакції.
Базовий принцип: Solana виділяє кожній транзакції типовий ліміт у 200 000 CU. Якщо ваша транзакція не містить інструкції SetComputeUnitLimit, саме цей ліміт буде застосовано. Для більшості простих переказів цього достатньо. Для складних транзакцій — ні.
Складові споживання compute units
Загальне споживання CU транзакцією складається з:
- Базова вартість кожної інструкції — фіксована плата за сам факт виклику інструкції в транзакції.
- Виконання програмної логіки — кожна операція BPF (виконання інструкцій віртуальної машини) тарифікується. Арифметика, розгалуження, виклики syscalls — усе має вагу в CU.
- CPI-виклики — кожен крос-програмний виклик додає накладні витрати: перевірка прав доступу, серіалізація та десеріалізація вхідних даних, створення нового контексту виконання.
- Обробка даних облікових записів — читання та запис в облікові записи тарифікуються пропорційно розміру даних.
- Syscalls — виклики операційної системи (логування, хешування, криптографічні операції) мають власні тарифи.
Методика розрахунку
Не намагайтеся рахувати CU вручну за тарифікаційною таблицею — це помилковий шлях для production-застосунків. Замість цього використовуйте інструментальне вимірювання:
- Додайте sol_log_compute_units() у критичні точки програми. Цей syscall записує поточне значення лічильника CU у лог транзакції. Розмістіть його перед кожним CPI-викликом і після завершення ключових циклів.
- Виміряйте дельту між точками. Різниця між двома сусідніми логами дає точне споживання CU конкретним фрагментом логіки.
- Просумуйте всі дельти та додайте запас. Рекомендований запас — 15–25% від виміряного значення. Цей запас компенсує варіативність, пов'язану з різними станами облікових записів (різна кількість елементів у масивах, різні гілки умов).
Очікуваний результат: ви отримуєте конкретне число — наприклад, 340 000 CU — яке стає вашим цільовим лімітом.
Типова помилка: встановлювати ліміт «з великим запасом» на рівні максимального дозволеного значення. Перевірте актуальний максимум у офіційній документації Solana runtime — історично цей параметр змінювався, і поточне значення треба підтвердити за первинним джерелом. Завищений ліміт не збільшує комісію за самі compute units, але може впливати на пріоритезацію транзакції та поведінку планувальника.
Використання computeBudget instructions
Передумови: клієнтська бібліотека @solana/web3.js версії 1.40+ або 2.x, підключення до RPC-вузла.
Compute budget керується двома спеціальними інструкціями, які додаються на початок транзакції (перед усіма іншими інструкціями):
SetComputeUnitLimit
Встановлює верхню межу CU для транзакції. Якщо транзакція споживе менше — оплата здійснюється лише за фактично спожиті одиниці. Якщо перевищить — транзакція впаде.
Приклад додавання інструкції через @solana/web3.js:
ComputeBudgetProgram.setComputeUnitLimit({ units: 340000 })
transaction.add(setComputeUnitLimitIx)
Критично: ця інструкція повинна бути першою в транзакції. Якщо розмістити її після інших інструкцій — вона буде проігнорована, і транзакція використає типовий ліміт 200 000 CU.
SetComputeUnitPrice
Встановлює ціну за одиницю compute (у micro-lamports). Це механізм пріоритезації: вища ціна підвищує шанс швидшого включення транзакції в блок під час навантаження мережі.
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50000 })
transaction.add(setComputeUnitPriceIx)
Правильний порядок у транзакції:
- SetComputeUnitLimit
- SetComputeUnitPrice
- Усі інші інструкції вашої транзакції
Ризик: встановлення надто високої ціни без потреби призводить до переплати. Формула загальної комісії: (фактично спожиті CU) × (ціна за CU) + базова комісія транзакції. Тобто ціна впливає на вартість фактично спожитих одиниць, а не на зарезервований ліміт.
Спосіб перевірки: після відправки транзакції перевірте поле meta.preBalances та meta.postBalances у відповіді RPC-методу getTransaction — різниця покаже фактичну сплачену комісію.
Тестування бюджету в local validator
Передумови: встановлений solana-cli (перевірте актуальну версію в офіційному репозиторії Solana), розгорнута локальна програма, підготовлений тестовий набір облікових записів.
Запуск local validator з логуванням
Запустіть локальний валідатор із виведенням логів програми:
solana-test-validator --log-program-messages
Цей параметр дозволяє бачити вивід sol_log_* у терміналі, включно з sol_log_compute_units().
Процедура тестування
- Підготуйте облікові записи в стані, що відповідає production. Розмір даних, кількість елементів у масивах, заповненість слотів — усе має бути реалістичним. Тест на порожніх облікових записах дасть занижені показники CU.
- Відправте транзакцію без SetComputeUnitLimit. Якщо транзакція проходить — типових 200 000 CU вистачило. Запишіть фактичне споживання з логів.
- Відправте транзакцію з SetComputeUnitLimit, встановленим на виміряне значення мінус 5%. Перевірте, що транзакція впадає з ComputationalBudgetExceeded. Це підтверджує, що ваш ліміт дійсно працює як межа.
- Відправте транзакцію з лімітом, що дорівнює виміряному значенню плюс 20%. Транзакція має пройти. Запишіть нове фактичне споживання — воно має збігатися з попереднім виміром (ліміт не впливає на фактичне споживання, лише на межу).
- Протестуйте граничні випадки. Максимально заповнений масив, найгірша гілка умов, кілька послідовних CPI-викликів. Зафіксуйте максимальне споживання CU серед усіх сценаріїв.
Очікуваний результат: ви маєте конкретне число — максимальне споживання CU серед усіх тестових сценаріїв — і розрахований ліміт із запасом.
Обмеження local validator: локальний валідатор не відтворює навантаження мережі, черг транзакцій та конкуренції за слоти. Він підходить для вимірювання CU, але не для тестування пріоритезації через SetComputeUnitPrice.
План відкату: якщо після розгортання виявиться, що ліміту не вистачає для якихось станів даних, ви можете тимчасово встановити ліміт на максимум (перевірте актуальне значення в документації) і паралельно провести додаткове вимірювання на devnet із реальними даними.
Моніторження реального споживання в production
Передумови: доступ до RPC-вузла з увімкненим журналюванням транзакцій або інтегрований моніторинг (наприклад, власний індексатор або стороннє рішення для аналітики Solana-транзакцій).
Отримання даних про споживання CU
Після підтвердження транзакції викличте getTransaction з відповідним параметром версії. У відповіді знайдіть:
- meta.computeUnitsConsumed — фактична кількість CU, спожитих транзакцією. Це ключове поле для моніторингу.
- Логи програми — містять вивід sol_log_compute_units(), якщо ви залишили ці виклики у production-збірці.
Налаштування моніторингу
- Зберігайте computeUnitsConsumed для кожної транзакції вашого застосунку. Це базова телеметрія. Зберігайте разом із ідентифікатором типу транзакції, timestamp та статусом (успіх або помилка).
- Встановіть алерти на аномальне споживання. Якщо типова транзакція споживає 340 000 CU, а раптом з'являються значення понад 450 000 — це сигнал. Можлива причина: зміна структури даних в обліковому записі (більше елементів у масиві), нова гілка логіки, або спроба атаки, що змушує програму виконувати додаткові ітерації.
- Відстежуйте частоту помилки ComputationalBudgetExceeded. Навіть одна така помилка на тисячу транзакцій означає, що ваш розрахований ліміт із запасом не покриває всі стани даних. Це прямий сигнал до перегляду бюджету.
- Моніторьте співвідношення «ліміт / фактичне споживання». Якщо ви встановили ліміт 500 000 CU, а фактичне споживання стабільно 340 000 CU — запас надмірний. Зниження ліміту до 420 000 CU зменшить ризик того, що транзакція займе зайвий час у слоті (хоча прямий вплив на throughput залежить від внутрішнього планувальника).
Діагностика при виявленні проблем
Якщо в production фіксується ComputationalBudgetExceeded:
- Отримайте повні логи транзакції, що впала. Знайдіть останній виклик sol_log_compute_units() перед падінням — він покаже, на якому етапі виконання вичерпався бюджет.
- Реконструюйте стан облікових записів на момент цієї транзакції (використовуючи історичні дані RPC або індексатор). Порівняйте з тестовими даними — найчастіша причина: production-дані більші за тестові.
- Збільште ліміт або оптимізуйте логіку. Якщо збільшення ліміту неможливе (близько до максимуму) — шукайте можливості оптимізації: заміна O(n²) на O(n), кешування проміжних результатів, розбиття транзакції на кілька.
Межа застосування: ця методика стосується виключно compute budget. Вона не вирішує проблеми перевищення розміру транзакції (1232 байти), ліміту на кількість облікових записів в одній транзакції або проблем із concurrent writes до облікових записів.
Наступний логічний крок: після стабілізації compute budget варто перевірити, чи ваші облікові записи не утримують надлишкові дані, що призводить до зайвого споживання CU. Це пов'язано з ефективним управлінням rent-exempt мінімумами — тема, розкрита в наступному матеріалі розділу.