Помилка 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 з попереднього кроку.
Кроки:
- Додайте інструкцію
SetComputeUnitLimitяк першу інструкцію транзакції. Значення встановіть із запасом 10–20% відносно виміряного споживання. Наприклад, якщоsimulateTransactionпоказав 180 000 CU, встановіть ліміт 200 000–216 000. - За необхідності додайте
SetComputeUnitPriceодразу після — це встановить пріоритетну комісію (priority fee), щоб валідатори надавали перевагу вашій транзакції. - Відправте транзакцію та перевірте результат.
Очікуваний результат: транзакція проходить, у логах видно, що спожито менше CU, ніж встановлено лімітом.
Обмеження: існує максимальна межа CU на транзакцію, яку встановлює кластер. Цю величину треба перевірити в актуальній документації Solana або через RPC-метод getRecentBlockhash (поле computeUnitLimit), оскільки вона змінювалася в ході оновлень мережі. Стандартне значення за замовчуванням — 200 000 CU, але максимальне може бути значно вищим.
Ризики:
- Зайвий бюджет — якщо встановити ліміт значно вищий за реальне споживання, ви переплачуєте комісію за зарезервовані, але невикористані CU. Солана стягує комісію саме за виділений ліміт, а не за фактичне споживання.
- Недостатній бюджет — якщо реальне споживання коливається залежно від даних (наприклад, цикл обробки змінної кількості елементів), запас у 10% може бути замалим. Для таких випадків використовуйте запас 30–50% або вимірюйте споживання на найгіршому сценарії вхідних даних.
Перевірка: після розгортання перевірте в експлорері (наприклад, SolanaFM або Solscan), що фактичне споживання CU не наближається до встановленого ліміту менш ніж на 5%.
Оптимізація гарячих шляхів
Якщо збільшення бюджету неможливе (досягнуто кластерну межу) або економічно недоцільне (занадто висока комісія), треба зменшити споживання CU саме в тій інструкції, яка виявлена як найбільш витратна.
Передумови: результати профілювання з першого кроку; доступ до вихідного коду програми на Rust/Anchor; локальне середовище для тестування (solana-test-validator або devnet).
Порядок дій:
- Ізолюйте гарячий шлях. Визначте конкретну функцію або блок коду всередині інструкції, що споживає найбільше CU. Використовуйте макрос
msg!з позначками часу або логування CU на межах блоків коду для локалізації. - Виявіть причину високого споживання. Найчастіші фактори:
- Надмірна десеріалізація: повторне розпакування одних і тих самих даних з акаунтів у межах однієї інструкції. Рішення — десеріалізувати один раз і передавати посилання.
- Необґрунтовані CPI-виклики: кожен крос-програмний виклик (Cross-Program Invocation) додає накладні витрати на серіалізацію/десеріалізацію та перевірку прав доступу. Перевірте, чи всі CPI дійсно необхідні в цій інструкції.
- Неефективні цикли: обробка масивів із складною логікою всередині. Розгляньте обмеження розміру масивів, винесення обчислень або попереднє обчислення агрегатів.
- Системні виклики (syscalls): кожен виклик
sol_log_*,sol_memcpyтощо має вартість у CU. Зменшення кількості логів у production-коді може суттєво зекономити бюджет.
- Внесіть цільову зміну. Зосередьтеся на одному факторі за раз. Не робіть кілька оптимізацій одночасно — це ускладнить вимірювання ефекту.
- Виміряйте різницю. Запустіть
simulateTransactionз тим самим набором вхідних даних до і після зміни. Фіксуйте різницю в CU. - Перевірте на регресію. Запустіть існуючі тести, щоб переконатися, що оптимізація не порушила логіку програми.
Очікуваний результат: зменшення споживання 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) і повернутися до неї, якщо нова модель створить більше проблем, ніж вирішить.
Загальний порядок відкату:
- Зафіксуйте поточну версію програми (commit hash, бінарний файл, program ID).
- Реалізуйте обраний варіант на devnet або локальному валідаторі з тестовими даними, що відтворюють production-сценарій з максимальним споживанням CU.
- Виміряйте CU-споживання нової версії та переконайтеся, що вона укладається в ліміт із запасом не менше 15%.
- Якщо жоден варіант не дає прийнятного результату — зафіксуйте це як обмеження платформи для цього типу операцій і документуйте його для команди продукту та користувачів.
Наступний логічний крок: після усунення помилки варто системно підійти до оптимізації CU у всій програмі — це розглядається в наступному матеріалі розділу.