Помилка compute limit exceeded означає, що транзакція вичерпала виділений бюджет обчислювальних одиниць (compute units, CU) до завершення всіх інструкцій. Рішення завжди полягає в одному з двох: збільшити бюджет або зменшити споживання. Нижче — покрокова діагностика та конкретні дії для кожного сценарію.

Діагностика: скільки CU споживає кожна інструкція

Перш ніж вносити будь-які зміни, треба точно визначити, яка саме інструкція перевищує ліміт і скільки CU вона споживає. Припущення тут не працюють — потрібні виміри.

Профілювання через simulateTransaction

Використовуйте RPC-метод simulateTransaction із прапорцем sigVerify: false. Це дозволяє отримати розбірку по інструкціях без підписання транзакції та без сплати комісій.

Передумови: зібрана транзакція, що викликає помилку; доступ до RPC-вузла з увімкненим simulateTransaction (більшість публічних RPC це підтримують).

Очікуваний результат: у відповіді RPC у полі value.unitsConsumed — загальне споживання CU, а в масиві value.accounts — зміни стану акаунтів. Для детального розбірки по інструкціях використовуйте параметр replaceRecentBlockhash та аналізуйте логи, які повертаються з рівнем логування computeUnits або вище.

Критичний нюанс: стандартна відповідь simulateTransaction не завжди розбиває споживання по кожній інструкції окремо. Для точного покрокового профілювання встановіть рівень логування computeUnits у запиті — тоді в логах з'являться рядки формату "Program ... consumed X of Y compute units" для кожної інструкції.

Ризик: simulateTransaction виконується на поточному слоті RPC-вузла. Якщо стан ланцюга змінився з моменту формування транзакції, результати можуть відрізнятися від реального виконання. Завжди перевіряйте актуальність recentBlockhash.

Виявлення найбільш витратних операцій

Після отримання логів побудуйте таблицю споживання по інструкціях:

Інструкція (порядковий номер) Програма Спожито CU Частка від загального бюджету
0 (computeBudget) Compute Budget 0 0%
1 Ваша програма ~150 000 75%
2 Ваша програма (CPI) ~60 000 30%

Якщо одна інструкція споживає понад 60–70% бюджету — це гарячий шлях, який потребує пріоритетної уваги. Якщо ж споживання розподілене відносно рівномірно, але сума перевищує ліміт — проблема в загальному обсязі роботи в межах однієї транзакції.

Типова помилка: фокусуватися на програмі, яка фігурує в повідомленні помилки, тоді як справжнє споживання може бути в попередній інструкції тієї ж транзакції. Завжди аналізуйте повний лог, а не лише останній рядок.

Збільшення compute budget через computeBudget

Найшвидший спосіб усунути помилку — явно вказати вищий ліміт через інструкції модуля Compute Budget. Це не прискорює виконання, але дозволяє транзакції завершитися, якщо реальне споживання не перевищує максимально допустиму межу кластера.

Передумови: клієнт, що формує транзакцію (наприклад, @solana/web3.js або @coral-xyz/anchor); розуміння поточного споживання CU з попереднього кроку.

Кроки:

  1. Додайте інструкцію SetComputeUnitLimit як першу інструкцію транзакції. Значення встановіть із запасом 10–20% відносно виміряного споживання. Наприклад, якщо simulateTransaction показав 180 000 CU, встановіть ліміт 200 000–216 000.
  2. За необхідності додайте SetComputeUnitPrice одразу після — це встановить пріоритетну комісію (priority fee), щоб валідатори надавали перевагу вашій транзакції.
  3. Відправте транзакцію та перевірте результат.

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

Обмеження: існує максимальна межа CU на транзакцію, яку встановлює кластер. Цю величину треба перевірити в актуальній документації Solana або через RPC-метод getRecentBlockhash (поле computeUnitLimit), оскільки вона змінювалася в ході оновлень мережі. Стандартне значення за замовчуванням — 200 000 CU, але максимальне може бути значно вищим.

Ризики:

  • Зайвий бюджет — якщо встановити ліміт значно вищий за реальне споживання, ви переплачуєте комісію за зарезервовані, але невикористані CU. Солана стягує комісію саме за виділений ліміт, а не за фактичне споживання.
  • Недостатній бюджет — якщо реальне споживання коливається залежно від даних (наприклад, цикл обробки змінної кількості елементів), запас у 10% може бути замалим. Для таких випадків використовуйте запас 30–50% або вимірюйте споживання на найгіршому сценарії вхідних даних.

Перевірка: після розгортання перевірте в експлорері (наприклад, SolanaFM або Solscan), що фактичне споживання CU не наближається до встановленого ліміту менш ніж на 5%.

Оптимізація гарячих шляхів

Якщо збільшення бюджету неможливе (досягнуто кластерну межу) або економічно недоцільне (занадто висока комісія), треба зменшити споживання CU саме в тій інструкції, яка виявлена як найбільш витратна.

Передумови: результати профілювання з першого кроку; доступ до вихідного коду програми на Rust/Anchor; локальне середовище для тестування (solana-test-validator або devnet).

Порядок дій:

  1. Ізолюйте гарячий шлях. Визначте конкретну функцію або блок коду всередині інструкції, що споживає найбільше CU. Використовуйте макрос msg! з позначками часу або логування CU на межах блоків коду для локалізації.
  2. Виявіть причину високого споживання. Найчастіші фактори:
    • Надмірна десеріалізація: повторне розпакування одних і тих самих даних з акаунтів у межах однієї інструкції. Рішення — десеріалізувати один раз і передавати посилання.
    • Необґрунтовані CPI-виклики: кожен крос-програмний виклик (Cross-Program Invocation) додає накладні витрати на серіалізацію/десеріалізацію та перевірку прав доступу. Перевірте, чи всі CPI дійсно необхідні в цій інструкції.
    • Неефективні цикли: обробка масивів із складною логікою всередині. Розгляньте обмеження розміру масивів, винесення обчислень або попереднє обчислення агрегатів.
    • Системні виклики (syscalls): кожен виклик sol_log_*, sol_memcpy тощо має вартість у CU. Зменшення кількості логів у production-коді може суттєво зекономити бюджет.
  3. Внесіть цільову зміну. Зосередьтеся на одному факторі за раз. Не робіть кілька оптимізацій одночасно — це ускладнить вимірювання ефекту.
  4. Виміряйте різницю. Запустіть simulateTransaction з тим самим набором вхідних даних до і після зміни. Фіксуйте різницю в CU.
  5. Перевірте на регресію. Запустіть існуючі тести, щоб переконатися, що оптимізація не порушила логіку програми.

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

Межа застосування: цей розділ зосереджений на усуненні конкретної помилки через цільову оптимізацію гарячого шляху. Загальні стратегії оптимізації CU у програмах розглядаються в окремому матеріалі.

Типова помилка: оптимізувати код, який не є гарячим шляхом. Якщо інструкція споживає 5 000 CU, а ліміт перевищує інша інструкція на 195 000 CU — оптимізація першої не вирішить проблему.

План відкату при неможливості оптимізації

Існують сценарії, коли ні збільшення бюджету (досягнуто кластерну межу), ні оптимізація коду (алгоритмічна складність фундаментально висока) не усувають помилку. У такому разі потрібен архітектурний відкат або зміна підходу.

Варіант 1. Розбиття транзакції на кілька.

  • Суть: замість однієї транзакції з N інструкціями — кілька послідовних транзакцій, кожна з яких укладається в CU-ліміт.
  • Обмеження: не підходить, якщо інструкції мають атомарні залежності (наприклад, перевірка стану та зміна стану мають відбутися в одній транзакції). Також збільшує загальну комісію та час виконання.
  • Відкат: якщо розбиття порушує атомарність бізнес-логіки — повернутися до єдиної транзакції та шукати інший підхід.

Варіант 2. Часткове винесення обчислень off-chain.

  • Суть: клієнт або сервер обчислює проміжні результати і передає їх у транзакції як дані. Програма лише перевіряє коректність і записує фінальний стан.
  • Обмеження: вимагає довіри до off-chain обчислювача або додаткових on-chain перевірок (що самі по собі споживають CU). Не підходить для операцій, де саме on-chain обчислення є ключовою цінністю.
  • Відкат: якщо перевірка коректності off-chain результатів on-chain виявиться дорожчою за оригінальне обчислення — відмовитися від підходу.

Варіант 3. Зміна моделі даних.

  • Суть: замість обробки великого обсягу даних в одній інструкції — попередньо агрегувати дані в окремих транзакціях і зберігати проміжний стан в акаунтах.
  • Обмеження: збільшує складність програми та кількість акаунтів, що може вплинути на інші аспекти (розмір транзакції, кількість підписів).
  • Відкат: зберегти попередню версію програми на окремому адресі (program ID) і повернутися до неї, якщо нова модель створить більше проблем, ніж вирішить.

Загальний порядок відкату:

  1. Зафіксуйте поточну версію програми (commit hash, бінарний файл, program ID).
  2. Реалізуйте обраний варіант на devnet або локальному валідаторі з тестовими даними, що відтворюють production-сценарій з максимальним споживанням CU.
  3. Виміряйте CU-споживання нової версії та переконайтеся, що вона укладається в ліміт із запасом не менше 15%.
  4. Якщо жоден варіант не дає прийнятного результату — зафіксуйте це як обмеження платформи для цього типу операцій і документуйте його для команди продукту та користувачів.

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

Джерела