Українська спільнота Solana: нові матеріали, безпека та подіїСпільнота Solana в TelegramПриєднатися →
Поглиблена розробка, помилки та production

Тестування й діагностика

Версії для прикладів у «Тестування й діагностика» перевірено 2 серпня 2026 року. Стабільна гілка Anchor v1 має релізи 1.0.x і орієнтується на Solana 3.x; Anchor v2 у документації позначений як alpha. Приклади для Anchor 0.29–0.32 залишаються лише…

0 підрозділів0 матеріалів на цьому рівніОновлено 1 серпня 2026

Версії для прикладів у «Тестування й діагностика» перевірено 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.

Цей розділ — практичний довідник із доведення Solana-програм до production. Тут зібрано інженерні підходи до локального тестування, діагностики типових помилок транзакцій, налаштування CI/CD та відтворення production-збоїв у контрольованому середовищі. Матеріал орієнтований на команди, які вже мають працюючий прототип і потребують стабільності на mainnet.

Як діагностувати transaction failed у Solana

Кроки отримання деталей помилки

Коли транзакція повертає статус failed, перше джерело інформації — сигнатура транзакції. Через будь-який RPC-ендпойнт викличте getTransaction з параметром commitment: confirmed та обов'язково передайте maxSupportedTransactionVersion: 0 (або відповідну версію, якщо використовуєте Address Lookup Tables). У відповіді шукайте поле meta.err — воно містить структурований об'єкт помилки, а не просто рядок.

Якщо meta.err дорівнює null, але транзакція не виконалась, перевірте meta.logMessages — іноді програма логує помилку через msg!, але не повертає error-код на рівні runtime. Також зверніть увагу на meta.preBalances та meta.postBalances: різниця покаже, чи були списані комісії навіть за невиконану транзакцію.

Класифікація помилок за типом

  • Runtime-помилкиInstructionError, TransactionError. Виникають на рівні виконання транзакції до входу в програму або під час інструкції. Мають числові коди, які розшифровуються через вихідний код Solana runtime.
  • Помилки програмиCustom (код 0x1771 для Anchor-помилок), InvalidInstructionData, InvalidAccountData. Генеруються самою програмою.
  • Інфраструктурні помилкиBlockhashNotFound, AccountInUse, TooManyAccountLocks. Пов'язані з станом мережі, а не з логікою програми.

Інструменти для автоматичної діагностики

Для систематичної діагностики у production варто побудувати пайплайн: зберігати сигнатури всіх відправлених транзакцій, періодично опитувати getSignatureStatuses для тих, що не підтвердились, і розбирати meta.err у структуровані логи. Це дозволяє агрегувати помилки за типом і виявляти сплески до скарг користувачів.

План відкату при масових відмовах

Якщо діагностика показує масовий збій одного типу (наприклад, всі транзакції повертають один і той самий InstructionError), негайно зупиніть відправку нових транзакцій з боку клієнта. Збережіть список сигнатур останніх невдалих транзакцій для подальшого аналізу. Перевірте, чи не оновилась версія програми на mainnet без вашого відома. Якщо проблема в стані акаунтів — не намагайтеся виправляти їх вручну, спочатку відтворіть сценарій на local validator.

Що означає blockhash expired і як це виправити

Причина помилки: життєвий цикл blockhash

Кожна транзакція в Solana посилається на recent blockhash — хеш останнього блоку. Цей хеш діє як лічильник часу: транзакція є валідною лише протягом приблизно 150 блоків (близько 60–90 секунд) після включення блоку з цим хешем у ланцюг. Якщо транзакцію не включено до цього вікна, вона відхиляється з помилкою BlockhashNotFound.

Автоматичне отримання свіжого blockhash

Перед кожною відправкою транзакції викликайте getLatestBlockhash з commitment: finalized. Не кешуйте blockhash довше ніж на 30 секунд. У бібліотеках на кшталт @solana/web3.js метод sendTransaction за замовчуванням автоматично підставляє свіжий blockhash, але лише якщо ви не передали його вручну через Transaction.recentBlockhash.

Патерни повторної відправки транзакцій

Якщо транзакція не підтверджується протягом 30–40 секунд, не чекайте повного expiry — отримайте новий blockhash і відправте транзакцію заново. Важливо: перед повторною відправкою перевірте статус оригінальної транзакції через getSignatureStatuses, щоб уникнути дублювання інструкцій (наприклад, подвійного списання токенів).

Коли blockhash expired вказує на іншу проблему

Якщо BlockhashNotFound виникає систематично для транзакцій, які відправляються миттєво зі свіжим blockhash, причина не в таймауті. Можливі фактичні причини: RPC-вузол повертає застарілий blockhash через затримку синхронізації; транзакція містить структурну помилку, через яку валідатори її відкидають ще до перевірки blockhash; або ви використовуєте getLatestBlockhash з commitment: processed, який може повертати хеш з orphan-гілки.

Як писати unit-тести для Anchor-програм

Структура тестового файлу в Anchor

Тести розміщуються у папці tests/ проєкту. Anchor автоматично запускає solana-test-validator перед виконанням тестів і зупиняє після. Базова структура: ініціалізація провайдера, отримання програми, виконання інструкцій через methods-об'єкт та перевірка стану акаунтів через fetch().

Тестування помилкових сценаріїв

Кожен unit-тест повинен перевіряти не лише щасливий шлях, а й всі гілки помилок. Для Anchor-програм використовуйте .catch(err => ...) або try/catch і перевіряйте, що помилка має правильний код та ім'я. Не перевіряйте текст повідомлення — він може змінюватись між версіями Anchor. Перевіряйте error.errorCode.code та error.errorCode.number.

Використання banks-client для локальних тестів

Для швидких unit-тестів без повного циклу запуску validator-процесу використовуйте solana-program-test та BanksClient. Це дозволяє запускати тести в тому ж процесі, що значно прискорює виконання. Однак banks-client не імітує всі аспекти runtime (зокрема, деякі аспекти prepaid compute units).

Покриття кордонних випадків

Обов'язково тестуйте: нульові баланси; акаунти, що не пройшли перевірку власника; перевищення ліміту compute units; спроби ініціалізації вже ініціалізованого акаунта; передачу неправильного PDA (з неправильним seeds). Кожен такий сценарій має бути окремим тестом з чіткою назвою, що описує перевіряну умову.

Як налаштувати local validator для тестування

Запуск solana-test-validator з потрібними параметрами

Мінімальна команда для запуску: solana-test-validator. Для реалістичного тестування додайте прапорці: --reset (очищає стан при кожному запуску), --bpf-program <address> <path_to_so></path_to_so></address> (завантажує вашу скомпільовану програму), --url https://api.mainnet-beta.solana.com (дозволяє запитувати акаунти з mainnet за потреби). Для програм, що взаємодіють з токенами, додайте --bpf-program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA target/deploy/token.so або використовуйте --clone для копіювання конкретних акаунтів.

Налаштування знімків для швидкого перезапуску

Якщо ваші тести потребують складного початкового стану (безліч акаунтів, токен-акаунти, ліквідність), створіть знімок: запустіть validator один раз, підготуйте стан, зупиніть процес і скопіюйте каталог test-ledger. При наступних запусках використовуйте --ledger test-ledger --reset — validator завантажить стан із знімка значно швидше, ніж створюватиме з нуля.

Обмеження local validator порівняно з mainnet

Local validator не імітує: реальну конкуренцію за слоти; мережеву затримку між валідаторами; поведінку інших програм на mainnet, які можуть модифікувати спільні акаунти; реальний розподіл лідерства. Тому транзакція, що проходить локально, може відхилитись на mainnet через AccountInUse або перевищення compute budget за умови навантаження.

Інтеграція з Anchor test framework

Anchor автоматично керує життєвим циклом local validator у командах anchor test та anchor test --skip-local-validator (якщо ви запускаєте validator окремо). Для кастомних параметрів validator-а використовуйте файл Anchor.toml, секція [test], поле startup_wait та додаткові аргументи через [test.validator].

Як використовувати simulation для перевірки транзакцій

simulateTransaction: що повертає

Метод simulateTransaction через RPC виконує транзакцію у пісочниці валідатора без фактичного запису в ланцюг. Відповідь містить: value.err (помилка або null), value.logs (логи програм), value.unitsConsumed (кількість витрачених compute units), value.returnData (дані, повернуті програмою, якщо використовується return у Rust-коді).

Аналіз compute unit consumption до відправки

Перед відправкою транзакції на mainnet викличте simulateTransaction і перевірте unitsConsumed. Якщо значення близьке до ліміту (за замовчуванням 200 000) або перевищує його — транзакція відхилиться. Використовуйте цю інформацію для встановлення адекватного computeUnitLimit через ComputeBudgetInstruction.setComputeUnitLimit(), щоб не переплачувати за зайві units.

Виявлення помилок без витрати SOL

Simulation не вимагає підписів від реальних акаунтів — замість них можна передати порожні підписи. Це дозволяє перевіряти транзакції в середовищах розробки без ризику витрат. Однак simulation не гарантує, що транзакція пройде на mainnet: вона не враховує стан акаунтів, який може змінитись між моментом simulation та фактичною відправкою.

Обмеження simulation для складних сценаріїв

Simulation не підтримує деякі типи інструкцій: ComputeBudgetInstruction.setComputeUnitPrice (приоритетна комісія) ігнорується; транзакції з Address Lookup Tables можуть симулюватись некоректно, якщо таблиці не існують на вузлі; CPI-виклики до програм, що залежать від конкретного блокчейн-стану (наприклад, поточного слоту), можуть давати інший результат у simulation порівняно з реальним виконанням.

Як читати та аналізувати логи програми

Формат логів Anchor-програм

Anchor генерує структуровані логи для кожної виконаної інструкції. Типовий потік: Program <address> invoke [1]</address> (вхід у програму), Program log: Instruction: <name></name> (назва інструкції), потім логи з msg!, і нарешті Program <address> success</address> або Program <address> failed: ...</address>. Число в invoke [N] вказує на глибину стеку CPI-викликів.

Розшифрування AnchorError кодів

Коли Anchor-програма повертає помилку, у логах з'являється рядок формату AnchorError occurred. Error Code: <codename>. Error Number: <number>. Error Message: <text>.</text></number></codename> Код Number — це числове представлення, яке також передається у meta.err як Custom(Number). Зверніть увагу: Anchor додає до ваших кастомних кодів зміщення 6000, тому ваш error код 0 у логах буде 6000.

Кастомні логи через msg! макрос

Макрос msg! у Rust-коді програми записує рядок у лог-потік. Використовуйте його для трасування виконання: логуйте значення ключових змінних, результати перевірок, точки входу в умовні гілки. Уникайте логування великих об'єктів — це збільшує розмір логу і може вплинути на compute budget. Пам'ятайте: msg! приймає форматний рядок як format!, але не підтримує всі форматні специфікатори (наприклад, {:#?} для Debug працює, але {:#x} може не підтримуватись у всіх версіях).

Інструменти для парсингу та фільтрації логів

Для локальної розробки логи виводяться прямо в термінал при запуску solana-test-validator з прапорцем --log або -l. Для аналізу production-логів використовуйте getTransaction з отриманням meta.logMessages і обробляйте їх програмно. Корисний патерн: фільтрувати логи за префіксом Program <your_program_address></your_program_address> та агрегувати помилки за типом.

Як налаштувати integration-тести з банківськими тестами

Архітектура banks-server для інтеграційного тестування

solana-program-test надає BanksServer — in-process реалізацію банкового runtime Solana. На відміну від solana-test-validator, який працює як окремий процес, banks-server запускається у тому ж процесі, що й тести. Це дає значний приріст швидкості, але з обмеженнями: неможливо підключитись зовнішнім RPC-клієнтом, немає реального мережевого стеку.

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

Banks-client дозволяє відправляти багато транзакцій послідовно в межах одного тесту. Кожна транзакція обробляється миттєво, і стан акаунтів оновлюється одразу. Це дозволяє тестувати багатокрокові сценарії: створення акаунта, депозит, вивід, закриття — все в одному тесті без затримок на підтвердження блоків.

Перевірка міжпрограмної взаємодії через CPI

Для тестування CPI-викликів у banks-server необхідно завантажити всі залежні програми. Використовуйте add_program при конфігурації ProgramTest для кожної програми, до якої ви звертаєтесь через CPI. Якщо CPI йде до системної програми (System, Token, Associated Token Account) — вони доступні за замовчуванням.

Тестування часових залежностей

Banks-server підтримує метод warp_to_slot(), який перемотує час вперед на вказану кількість слотів. Це дозволяє тестувати сценарії, що залежать від часу: закінчення термінів, розблокування, зміна стану після певної кількості слотів. Без цього механізму тестування часових умов було б неможливим в ізольованому середовищі.

Як діагностувати помилку InstructionError у Solana

Коди InstructionError та їх значення

InstructionError — це обгортка, яка містить індекс інструкції (яка саме інструкція в транзакції завершилась помилкою) та внутрішній код. Найпоширеніші внутрішні коди: 0GenericError (загальна помилка програми), 1InvalidArgument, 2InvalidInstructionData, 3InvalidAccountData, 4AccountDataTooSmall, 5InsufficientFunds, 6IncorrectProgramOwner, 7MissingRequiredSignature, 8AccountAlreadyInUse, 9AccountNotProgramOwned.

Найпоширеніші причини: insufficient funds, invalid account

InsufficientFunds (код 5) означає, що акаунт-платник не має достатньо lamports для виконання транзакції з урахуванням комісії. Перевірте meta.preBalances та порівняйте з очікуваною комісією. InvalidAccountData (код 3) вказує на те, що дані акаунта не відповідають очікуваній структурі — можливо, акаунт не ініціалізовано, або його дані було пошкоджено.

Послідовна діагностика за кодом

Алгоритм: 1) Визначте індекс інструкції з помилкою — це покаже, яка саме інструкція в багатоінструкційній транзакції завершилась невдало. 2) Отримайте внутрішній код. 3) Перевірте логи програми для цієї інструкції — вони можуть містити додатковий контекст. 4) Якщо внутрішній код Custom(N) — це помилка вашої програми, розшифруйте N з урахуванням зміщення Anchor (відніміть 6000) або за таблицею ваших error-кодів.

Типові помилки інтерпретації InstructionError

Поширена помилка — ігнорування індексу інструкції. Якщо транзакція містить три інструкції, а InstructionError вказує індекс 2, проблема не в першій інструкції, навіть якщо логічно саме вона мала б викликати помилку. Інша помилка — плутанина між InstructionError та помилками на рівні транзакції (наприклад, TransactionError::BlockhashNotFound не є InstructionError і не має індексу інструкції).

Як працювати з Anchor logs та помилками

Формат AnchorError: код, ім'я, повідомлення

Повний формат AnchorError у логах: AnchorError thrown in <instruction_name>. Error Code: <code_name>. Error Number: <number>. Error Message: <message>. Origin: <origin_type>.</origin_type></message></number></code_name></instruction_name> Поле Origin вказує, де саме виникла помилка: Caller (на боці клієнта), Program (всередині програми) або Constraint (при невиконанні Anchor-обмежень).

Кастомні error коди в Anchor

У Anchor-програмі помилки визначаються через #[error_code] атрибут над enum. Кожен варіант отримує автоматичний числовий код, починаючи з 6000. Не покладайтесь на конкретні числа — використовуйте імена кодів для діагностики. Якщо ви додали новий варіант error-enum посередині списку, усі наступні коди зсунуться — це часта причина плутанини при діагностиці.

Перехоплення помилок на боці клієнта

У TypeScript-клієнті Anchor-помилки мають тип AnchorError. Перевіряйте error instanceof AnchorError перед обробкою. Доступні поля: error.errorCode.code (рядок), error.errorCode.number (число), error.errorMessage. Не використовуйте instanceof для перевірки конкретних error-типів — усі Anchor-помилки мають один клас, розрізняйте їх за errorCode.code.

Дебаггінг з Anchor logs у local validator

Запустіть solana-test-validator з прапорцем --log - (логи в stdout) або --log <file></file>. Anchor-логи будуть виводитись у реальному часі. Для фільтрації використовуйте grep за адресою вашої програми. Якщо логів забагато, додайте RUST_LOG=solana_runtime::instruction_processor=trace для детальнішого трасування на рівні runtime (увага: це значно збільшує обсяг виводу).

Як відтворити production-помилку в local validator

Експорт стану акаунтів з mainnet

Використовуйте solana account <address> --output json</address> для отримання даних акаунта з mainnet. Збережіть вивід у файл. Для завантаження у local validator використовуйте прапорець --account <address> <data_file> <lamports></lamports></data_file></address> при запуску solana-test-validator. Для акаунтів з великим обсягом даних (наприклад, великих AMM-пулів) цей підхід працює надійно.

Відтворення послідовності транзакцій

Отримайте історію транзакцій, що призвели до помилки, через getSignaturesForAddress та getTransaction. Відтворіть їх послідовно у local validator, завантаживши попередній стан акаунтів. Це дозволяє точно визначити, на якому кроці виникає збій. Уважно стежте за тим, щоб відтворювати транзакції в тому ж порядку і з тим самим станом.

Використання дампів для аналізу

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

Обмеження відтворення складних станів

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

Як налаштувати CI/CD для тестування Solana-програм

Конфігурація GitHub Actions для Anchor

Базовий пайплайн: крок checkout, встановлення Rust та Solana CLI, встановлення Anchor, виконання anchor build, виконання anchor test. Встановлюйте фіксовані версії інструментів через agave-install init <version></version> та avm install <version></version> — це запобігає непередбачуваним змінам поведінки між запусками.

Запуск local validator у CI

Якщо ви використовуєте кастомні параметри validator-а або окремий запуск (не через anchor test), запускайте solana-test-validator у фоновому режимі та додайте крок очікування готовності (наприклад, цикл з solana cluster-version до успішної відповіді). Встановіть таймаут — validator може не запуститись через нестачу пам'яті в CI-середовищі.

Автоматичне перевірка coverage

Для вимірювання покриття коду Rust-програм використовуйте cargo-llvm-cov. Інтегруйте його у CI-пайплайн: зберіть програму з прапорцями coverage, запустіть тести, згенеруйте звіт. Встановіть мінімальний поріг coverage (наприклад, 70%) і робіть збій пайплайну, якщо поріг не досягнуто. Уважно стежте за розміром артефактів coverage — вони можуть бути значними.

Інтеграція з linter та форматувальниками

Додайте до CI: cargo clippy -- -D warnings (забороняє попередження), cargo fmt --check (перевіряє форматування). Для Anchor-специфічних перевірок використовуйте anchor check (якщо доступний у вашій версії). Запускайте linter перед білдом — це швидше, ніж чекати завершення компіляції для виявлення стилістичних проблем.

Як діагностувати помилку AccountInUse

Причина: одночасний запис до одного акаунта

AccountInUse виникає, коли дві або більше транзакцій у одному блоці намагаються записати в один і той самий акаунт. Solana runtime дозволяє паралельне виконання транзакцій, але якщо транзакції мають перетин по writable-акаунтах, вони не можуть бути оброблені паралельно і одна з них відхиляється.

Виявлення конфліктуючих транзакцій

У логах local validator при виникненні AccountInUse вказується, який саме акаунт викликав конфлікт. На mainnet діагностика складніша: перевірте, чи відправляє ваш сервіс кілька транзакцій, що записують в один акаунт, у короткий проміжок часу. Використовуйте getSignatureStatuses для визначення, чи потрапили обидві транзакції в один блок.

Патерни уникнення AccountInUse

Основні підходи: серіалізація записів до одного акаунта (відправляйте наступну транзакцію лише після підтвердження попередньої); використання PDA-акаунтів як буферів (кожен користувач або операція працює з окремим PDA, а фінальне агрегування відбувається окремо); використання secp256k1 програми для off-chain агрегування підписів з подальшою одною транзакцією.

Коли AccountInUse є симптомом іншої проблеми

Якщо ви відправляєте транзакції послідовно з очікуванням підтвердження, але все одно отримуєте AccountInUse, перевірте: чи не відправляє інший екземпляр вашого сервісу транзакції з тим самим акаунтом; чи не використовуєте ви skipPreflight, який може приховати іншу помилку; чи не є проблема в тому, що попередня транзакція ще не завершена обробку (використовуйте getSignatureStatuses з commitment: finalized для надійного визначення статусу).

Джерела