Simulation дозволяє виконати транзакцію на валідаторі без фактичного запису в леджер і без витрати SOL. Для production-застосунків це основний інструмент перевірки перед відправкою: він дає змогу виявити помилки, оцінити споживання compute units та перевірити повернуті дані, зберігаючи кошти користувача.
У цій інструкції розібрано метод simulateTransaction з боку RPC, формат його відповіді, практичні схеми аналізу ресурсів та відомі обмеження, які впливають на достовірність результату.
simulateTransaction: що повертає
Метод simulateTransaction приймає серіалізовану транзакцію та набір параметрів конфігурації. Два найважливіші прапорці:
- sigVerify — якщо
true, валідатор перевіряє криптографічні підписи. У більшості випадків для швидкої перевірки логіки достатньоfalse, що суттєво зменшує навантаження. - replaceRecentBlockhash — якщо
true, валідатор замінюєrecent_blockhashу транзакції на актуальний перед симуляцією. Це корисно, коли транзакція була сформована давно, але її логіка досі актуальна.
У відповідь RPC повертає об'єкт value, який містить кілька ключових полів:
| Поле | Тип | Призначення |
|---|---|---|
err |
null | об'єкт помилки |
Якщо null — транзакція виконалась успішно в рамках симуляції. Інакше містить структурований опис помилки (наприклад, InstructionError з індексом інструкції та кодом). |
logs |
масив рядків | Лог-вивід програм, аналогічний до того, що генерується при реальному виконанні. Корисний для діагностики, але не замінює повноцінний аналіз логів, який розглядається окремо. |
unitsConsumed |
число | Кількість compute units, витрачених під час симуляції. Це орієнтовне значення для встановлення ліміту в computeBudget. |
accounts |
масив об'єктів (за запитом) | Стан акаунтів після симуляції. Повертається лише якщо у конфігурації вказано encoding та масив accounts.addresses. Дозволяє перевірити зміну даних без виконання транзакції. |
returnData |
об'єкт (за наявності) | Дані, повернуті інструкцією через sol_log_data. Містить data (Base58) та programId. |
Типова відповідь успішної симуляції виглядає так:
{ "err": null, "logs": [...], "unitsConsumed": 12450, "returnData": null }
Важливо розуміти: поле err у симуляції відповідає саме за логіку виконання. Відсутність помилки тут не гарантує, що транзакція пройде при реальній відправці — про це детально у розділі обмежень.
Аналіз compute unit consumption до відправки
Точне встановлення ліміту compute units через інструкцію ComputeBudget дозволяє зменшити комісію транзакції та уникнути ситуацій, коли транзакція відхиляється через перевищення ліміту. Симуляція — основне джерело даних для цього.
Базова схема оцінки
- Сформуйте транзакцію без інструкції
ComputeBudget(або з мінімальним лімітом). - Викличте
simulateTransactionізsigVerify: false. - Значення
unitsConsumedз відповіді використайте як базу для розрахунку фінального ліміту.
Чому не можна брати значення один в один
Реальне споживання compute units може відрізнятися від симульованого з кількох причин:
- Зміна стану між симуляцією та відправкою. Якщо інший агент змінив дані акаунтів, які читає ваша транзакція, гілка виконання може змінитися, і витрати зростуть.
- Час виконання CPI. Зовнішні виклики (CPI) до програм, які ви не контролюєте, можуть мати різну тривалість залежно від внутрішнього стану.
- Попередні інструкції в тій самій транзакції. Якщо ваша транзакція містить кілька інструкцій, і перша змінює стан, який впливає на другу, симуляція враховує це, але лише в межах поточного стану леджера на момент виклику.
Практична формула
Для production-сценаріїв рекомендується додавати запас поверх значення unitsConsumed. Типовий підхід — додати 10–20% залежно від стабільності виконання програми:
- Для транзакцій із детермінованою логікою (без умовних гілок, що залежать від зовнішнього стану) — запас 5–10%.
- Для транзакцій із CPI до сторонніх програм або умовними гілками — запас 15–25%.
- Абсолютний максимум не повинен перевищувати 1 400 000 CU (з урахуванням резерву для самого runtime).
Після встановлення ліміту проведіть повторну симуляцію вже з фінальною транзакцією (включно з ComputeBudget), щоб переконатися, що ліміт не обрізає виконання.
Виявлення помилок без витрати SOL
Головна цінність симуляції — можливість діагностувати помилки до відправки. Кожна невдала відправка на mainnet-beta коштує базову комісію транзакції (навіть якщо транзакція відхилена). При масових операціях або складних інструкціях ці витрати стають відчутними.
Типові помилки, які виявляє симуляція
- InstructionError з кодами помилок програми. Наприклад,
InstructionError [0, Custom(6001)]вказує на кастомну помилку з кодом 6001 у першій інструкції. Це дозволяє точно локалізувати проблему без аналізу логів валідатора. - AccountInUse. Акаунт вже заблокований іншою транзакцією в поточному слоті. Симуляція може показати це, хоча реальна ситуація залежить від моменту відправки.
- InsufficientFunds. На акаунті, що сплачує комісію, недостатньо SOL. Симуляція перевіряє баланс на момент виклику.
- PreinstructionFailure для ComputeBudget. Неправильно встановлені параметри бюджету (наприклад, ліміт менший за мінімально допустимий).
Алгоритм перевірки перед відправкою
- Симулюйте з
sigVerify: false. Швидка перевірка логіки. Якщоerrнеnull— розбирайте помилку, не відправляйте. - Симулюйте з
sigVerify: true. Перевірка підписів. Це повільніше, але виявляє проблеми з серіалізацією підписів та несупадіння підписувачів. - Перевірте
unitsConsumed. Якщо значення близьке до встановленого ліміту або перевищує його — збільште ліміт і поверніться до кроку 1. - Якщо всі перевірки пройдені — відправляйте транзакцію через
sendTransaction.
Цей підхід не гарантує 100% успіху відправки, але усуває переважну більшість помилок, які можна виявити на стороні клієнта.
Обмеження simulation для складних сценаріїв
Symulation виконується в ізольованому контексті на одному валідаторі. Це створює фундаментальні обмеження, які треба враховувати при проектуванні перевірок.
Стан не фіксується
Зміни, які симуляція вносить у стан акаунтів, не записуються в леджер. Це означає, що неможливо симулювати ланцюжок транзакцій, де друга залежить від результату першої, викликавши simulateTransaction послідовно. Кожен виклик починається з одного й того ж стану леджера.
Для тестування ланцюжків використовуйте local validator, де транзакції виконуються реально. Це окрема тема, розглянута в матеріалі про налаштування local validator для тестування.
Блокування акаунтів (account locking)
Під час реальної обробки в банку транзакцій (transaction processing pipeline) валідатор блокує акаунти, що записуються, на рівні всього кластера. Симуляція не бере участь у цьому механізмі. Тому помилка AccountInUse, яку показує симуляція, базується на поточному стані банку на конкретному валідаторі, а не на реальній ситуації в момент потрапляння транзакції в банк.
Недетерміновані програми
Якщо програма залежить від даних, які змінюються між симуляцією та відправкою (курси оракулів, стан пулів ліквідності, nonce-значення), результат симуляції може бути невалідним до моменту реальної відправки. У таких випадках симуляція підтверджує лише коректність структури транзакції, а не її семантичний результат.
Обмеження на запитувані акаунти
Параметр accounts.addresses у конфігурації симуляції дозволяє отримати стан акаунтів після виконання. Проте є обмеження на розмір відповіді та кількість акаунтів. Для транзакцій, що зачіпають десятки акаунтів (типово для DeFi-операцій із кількома пулами), запит усіх акаунтів може призвести до перевищення ліміту розміру відповіді RPC-вузла.
MEV та порядок транзакцій
Симуляція не моделює порядок виконання транзакцій у банку. Для сценаріїв, де результат залежить від порядку (арбітраж, ліквідації, sniping), симуляція дає лише верхню оцінку ресурсів, але не передбачає реальний результат виконання.
Коли симуляції недостатньо
- Транзакції, що залежать від попередніх транзакцій у тому ж блоці.
- Сценарії з критичною залежністю від точного стану пулу чи оракула в момент виконання.
- Тестування edge-кейсів, що вимагають специфічного стану леджера, який неможливо відтворити на mainnet-beta.
У цих випадках комбінуйте симуляцію з тестуванням на local validator та аналізом логів програми для повної діагностики.