Складна транзакція на 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-застосунків. Замість цього використовуйте інструментальне вимірювання:

  1. Додайте sol_log_compute_units() у критичні точки програми. Цей syscall записує поточне значення лічильника CU у лог транзакції. Розмістіть його перед кожним CPI-викликом і після завершення ключових циклів.
  2. Виміряйте дельту між точками. Різниця між двома сусідніми логами дає точне споживання CU конкретним фрагментом логіки.
  3. Просумуйте всі дельти та додайте запас. Рекомендований запас — 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)

Правильний порядок у транзакції:

  1. SetComputeUnitLimit
  2. SetComputeUnitPrice
  3. Усі інші інструкції вашої транзакції

Ризик: встановлення надто високої ціни без потреби призводить до переплати. Формула загальної комісії: (фактично спожиті 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().

Процедура тестування

  1. Підготуйте облікові записи в стані, що відповідає production. Розмір даних, кількість елементів у масивах, заповненість слотів — усе має бути реалістичним. Тест на порожніх облікових записах дасть занижені показники CU.
  2. Відправте транзакцію без SetComputeUnitLimit. Якщо транзакція проходить — типових 200 000 CU вистачило. Запишіть фактичне споживання з логів.
  3. Відправте транзакцію з SetComputeUnitLimit, встановленим на виміряне значення мінус 5%. Перевірте, що транзакція впадає з ComputationalBudgetExceeded. Це підтверджує, що ваш ліміт дійсно працює як межа.
  4. Відправте транзакцію з лімітом, що дорівнює виміряному значенню плюс 20%. Транзакція має пройти. Запишіть нове фактичне споживання — воно має збігатися з попереднім виміром (ліміт не впливає на фактичне споживання, лише на межу).
  5. Протестуйте граничні випадки. Максимально заповнений масив, найгірша гілка умов, кілька послідовних CPI-викликів. Зафіксуйте максимальне споживання CU серед усіх сценаріїв.

Очікуваний результат: ви маєте конкретне число — максимальне споживання CU серед усіх тестових сценаріїв — і розрахований ліміт із запасом.

Обмеження local validator: локальний валідатор не відтворює навантаження мережі, черг транзакцій та конкуренції за слоти. Він підходить для вимірювання CU, але не для тестування пріоритезації через SetComputeUnitPrice.

План відкату: якщо після розгортання виявиться, що ліміту не вистачає для якихось станів даних, ви можете тимчасово встановити ліміт на максимум (перевірте актуальне значення в документації) і паралельно провести додаткове вимірювання на devnet із реальними даними.

Моніторження реального споживання в production

Передумови: доступ до RPC-вузла з увімкненим журналюванням транзакцій або інтегрований моніторинг (наприклад, власний індексатор або стороннє рішення для аналітики Solana-транзакцій).

Отримання даних про споживання CU

Після підтвердження транзакції викличте getTransaction з відповідним параметром версії. У відповіді знайдіть:

  • meta.computeUnitsConsumed — фактична кількість CU, спожитих транзакцією. Це ключове поле для моніторингу.
  • Логи програми — містять вивід sol_log_compute_units(), якщо ви залишили ці виклики у production-збірці.

Налаштування моніторингу

  1. Зберігайте computeUnitsConsumed для кожної транзакції вашого застосунку. Це базова телеметрія. Зберігайте разом із ідентифікатором типу транзакції, timestamp та статусом (успіх або помилка).
  2. Встановіть алерти на аномальне споживання. Якщо типова транзакція споживає 340 000 CU, а раптом з'являються значення понад 450 000 — це сигнал. Можлива причина: зміна структури даних в обліковому записі (більше елементів у масиві), нова гілка логіки, або спроба атаки, що змушує програму виконувати додаткові ітерації.
  3. Відстежуйте частоту помилки ComputationalBudgetExceeded. Навіть одна така помилка на тисячу транзакцій означає, що ваш розрахований ліміт із запасом не покриває всі стани даних. Це прямий сигнал до перегляду бюджету.
  4. Моніторьте співвідношення «ліміт / фактичне споживання». Якщо ви встановили ліміт 500 000 CU, а фактичне споживання стабільно 340 000 CU — запас надмірний. Зниження ліміту до 420 000 CU зменшить ризик того, що транзакція займе зайвий час у слоті (хоча прямий вплив на throughput залежить від внутрішнього планувальника).

Діагностика при виявленні проблем

Якщо в production фіксується ComputationalBudgetExceeded:

  1. Отримайте повні логи транзакції, що впала. Знайдіть останній виклик sol_log_compute_units() перед падінням — він покаже, на якому етапі виконання вичерпався бюджет.
  2. Реконструюйте стан облікових записів на момент цієї транзакції (використовуючи історичні дані RPC або індексатор). Порівняйте з тестовими даними — найчастіша причина: production-дані більші за тестові.
  3. Збільште ліміт або оптимізуйте логіку. Якщо збільшення ліміту неможливе (близько до максимуму) — шукайте можливості оптимізації: заміна O(n²) на O(n), кешування проміжних результатів, розбиття транзакції на кілька.

Межа застосування: ця методика стосується виключно compute budget. Вона не вирішує проблеми перевищення розміру транзакції (1232 байти), ліміту на кількість облікових записів в одній транзакції або проблем із concurrent writes до облікових записів.

Наступний логічний крок: після стабілізації compute budget варто перевірити, чи ваші облікові записи не утримують надлишкові дані, що призводить до зайвого споживання CU. Це пов'язано з ефективним управлінням rent-exempt мінімумами — тема, розкрита в наступному матеріалі розділу.

Джерела