Кожна транзакція в Solana проходить через валідацію підписів, перевірку стану акаунтів, виконання інструкцій і сплату комісій. Помилка на будь-якому з цих етапів призводить до відхилення всієї транзакції — Solana гарантує атомарність. Нижче наведено системну класифікацію помилок, їхні причини та конкретні кроки для перевірки й усунення.
Версії для прикладів у «Типові помилки транзакцій у Solana та їх вирішення» перевірено 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 CLI 1.18.26, @solana/web3.js 1.95.3, @coral-xyz/anchor 0.30.1, Rust 1.77.0, кластер Devnet.
Помилки підпису
Підпис підтверджує, що власник ключа санкціонував транзакцію. Валідатор перевіряє кожен підпис криптографічно, тому помилки тут є бінарними — підпис або валідний, або ні.
Signature verification failed
Підпис не відповідає повідомленню транзакції. Це означає, що дані, які були підписані, відрізняються від даних, що надійшли до валідатора.
Типові причини:
- Транзакцію було змінено після підписання (додано інструкцію, змінено recent_blockhash).
- Ключпару було серіалізовано некоректно — наприклад, передано публічний ключ замість повного keypair.
- Використано keypair від іншої мережі (mainnet замість devnet) — сам по собі це не змінює підпис, але recent_blockhash з іншого кластеру зробить підпис невалідним.
Концептуальний приклад перевірки (Node.js, Devnet):
const { Keypair, Transaction, SystemProgram, Connection } = require("@solana/web3.js");
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const payer = Keypair.generate();
const tx = new Transaction();
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.add(SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: payer.publicKey, lamports: 1000 }));
tx.sign(payer);
// Якщо після tx.sign() змінити tx.instructions — підпис стане невалідним
const sig = await connection.sendRawTransaction(tx.serialize());
await connection.confirmTransaction(sig, "confirmed");
Як перевірити: порівняйте serialized message до й після підписання. Якщо байти відрізняються — підпис буде невалідним. Ніколи не модифікуйте транзакцію після виклику sign().
Missing required signer
Інструкція очікує підпис певного акаунта, але транзакція його не містить. У метаданих інструкції акаунт позначено як signer = true, але у масиві signatures транзакції відповідного підпису немає.
Типові причини:
- Акаунт передано до інструкції, але tx.sign() не викликано з відповідним keypair.
- У Anchor-програмі аргумент має тип Signer, але передано звичайний AccountInfo без підпису.
- PDA (Program Derived Address) передано як signer, але invoke_signed не отримав правильний масив seeds.
Концептуальний приклад (Anchor, Rust):
#[derive(Accounts)]
pub struct InitializeContext<'info> {
#[account(mut)]
pub authority: Signer<'info>,
pub state: Account<'info, MyState>,
}
// Якщо клієнт не підпише authority — транзакція поверне MissingRequiredSignature
Як перевірити: у логах транзакції знайдіть рядок з помилкою та перевірте, який саме акаунт-індекс вимагає підпису. Потім переконайтеся, що цей keypair передано до tx.sign() або signers при відправці.
Помилки стану акаунтів
Кожна інструкція декларує, які акаунти потрібні та з якими правами доступу. Runtime перевіряє ці декларації до виконання.
Account does not exist
Транзакція посилається на акаунт, якого немає в леджері за вказаною адресою. Акаунт вважається таким, що існує, якщо він має додатний баланс лампортів або раніше був створений через системну програму.
Типові причини:
- Акаунт не ініціалізовано — викликано інструкцію програми до створення акаунта через SystemProgram.createAccount або Anchor-директиву init.
- Акаунт закрито (баланс знято до нуля), але транзакція все ще намагається його використати.
- Помилково сформовано адресу PDA — seeds відрізняються від тих, що використовувалися при створенні.
Як перевірити безпечно: викличте connection.getAccountInfo(address) на Devnet. Якщо результат null — акаунт не існує. Перевірте seeds PDA або додайте крок ініціалізації до транзакції.
Account is not writable
Інструкція намагається змінити дані акаунта, але цей акаунт не позначено як writable у транзакції.
Типові причини:
- У @solana/web3.js акаунт додано до інструкції без прапорця isWritable: true.
- У Anchor структурі акаунтів не вказано #[account(mut)].
- Акаунт є системною програмою або іншою програмою, яка не може бути модифікована.
Концептуальний приклад (web3.js):
// Неправильно — акаунт передано як read-only
transaction.add(new TransactionInstruction({
keys: [{ pubkey: stateAccount, isSigner: false, isWritable: false }],
programId: myProgramId,
data: instructionData,
}));
// Правильно — isWritable: true, якщо програма записує дані
keys: [{ pubkey: stateAccount, isSigner: false, isWritable: true }]
Очікуваний результат після виправлення: транзакція проходить перевірку writable-статусу і переходить до етапу виконання.
Помилки виконання
Якщо підписи та стан акаунтів валідні, runtime передає інструкції до відповідних програм. Помилка на цьому етапі означає, що програма повернула ненульовий код або запанікувала.
Instruction fell back
У контексті Solana цей термін означає, що виконання інструкції завершилося невдало, і runtime відкотив усі зміни, зроблені цією інструкцією та попередніми в межах транзакції. Solana не має механізму «fallback» для альтернативного виконання — атомарність означає повне відкатування.
Типові причини:
- Програма викликає panic! у Rust-коді.
- Логіка програми повертає помилку через Err() або msg!("error") з Anchor.
- CPI (Cross-Program Invocation) до іншої програми завершився помилкою, і викликаюча програма не обробила її.
Як перевірити: у логах транзакції шукайте рядок Program [адреса] failed. Попередні рядки Program log: містять повідомлення програми, які вказують на конкретну причину.
Custom program error: як читати коди
Anchor-програми повертають помилки як цілі числа. У логах це виглядає як Custom program error: 0x... у шістнадцятковому форматі.
Алгоритм читання:
- Знайдіть у логах рядок Custom program error: 0xNN.
- Переведіть шістнадцяткове значення у десяткове (наприклад, 0x6003 = 24579).
- Відніміть 6000 (Anchor-офсет): 24579 - 6000 = 579.
- Знайдіть цей номер у файлі errors.rs вашої програми або в згенерованому idl.
Концептуальний приклад (Anchor, errors.rs):
#[error_code]
pub enum MyError {
#[msg("Недостатньо балансу для операції")]
InsufficientBalance = 579,
}
// У логах: Custom program error: 0x6003
// 0x6003 = 24579; 24579 - 6000 = 579 → InsufficientBalance
Важливо: цей метод працює лише для Anchor-програм. Для програм без Anchor код помилки відповідає тому, що програма повернула через solana_program::program_error::ProgramError або власний enum.
Помилки комісій та лімітів
Кожна транзакція сплачує комісію за обчислення та зберігання. Перевищення лімітів або нестача коштів призводять до відхилення до виконання.
Insufficient funds
На акаунті, що сплачує комісію, недостатньо SOL для покриття базової комісії транзакції та додаткових витрат (створення акаунтів, збільшення розміру даних).
Типові причини:
- Баланс payer-акаунта менший за baseFee + sumOfAccountBalances.
- Транзакція створює новий акаунт, але не передано достатньо lamports для rent-exempt мінімуму.
- Баланс змінився між формуванням транзакції та її відправкою (інша транзакція витратила кошти).
Як перевірити: викличте connection.getBalance(payerPublicKey) і порівняйте з очікуваною комісією. Для створення акаунта перевірте connection.getMinimumBalanceForRentExemption(dataSize).
ComputeBudgetExceeded
Транзакція перевищила виділений обчислювальний бюджет. За замовчуванням це 200 000 compute units на Devnet (може змінюватися, перевіряйте актуальне значення в документації).
Типові причини:
- Цикл обробки великого масиву даних у програмі.
- Забагато CPI-викликів у межах однієї транзакції.
- Складна десеріалізація або криптографічні операції.
Концептуальний приклад збільшення бюджету (web3.js):
const { ComputeBudgetProgram } = require("@solana/web3.js");
const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
units: 400000,
});
transaction.add(modifyComputeUnits);
// Додати цю інструкцію ПЕРШОЮ в транзакцію
Обмеження: максимальний бюджет обмежений кластером. На Devnet це зазвичай 1 400 000 compute units. Збільшення бюджету також збільшує комісію транзакції.
Як перевірити: у логах транзакції знайдіть рядок Compute budget exceeded. Перевірте consumed units у метаданих транзакції та порівняйте з виділеним бюджетом.
Як діагностувати
Діагностика в Solana будується на двох інструментах: локальних логах програми та блокчейн-експлорері. Мета — від типового коду помилки перейти до конкретного рядка програми або параметра транзакції, що викликав відхилення.
solana logs — читання логів програми
Якщо ви запускаєте локальний валідатор (solana-test-validator), ви отримуєте логи програми в реальному часі у терміналі. Це найшвидший спосіб ітерації під час розробки.
Кроки:
- Запустіть валідатор: solana-test-validator --reset.
- У іншому терміналі відправте транзакцію з клієнта або тестів.
- У логах валідатора знайдіть рядки Program log: — це msg! з вашої програми.
- Рядок Program [адреса] failed: вказує на місце помилки.
Обмеження: локальний валідатор не відтворює всі умови мережі (конкурентні транзакції, реальний навантаження). Після локальної перевірки обов'язково тестуйте на Devnet.
Explorer: детальний перегляд помилки
Solana Explorer (explorer.solana.com) дозволяє переглянути повну інформацію про транзакцію в кластері, включно з усіма логами програм, статусом кожної інструкції та використаними ресурсами.
Кроки для Devnet:
- Отримайте signature транзакції після відправки (навіть якщо вона відхилена, signature існує).
- Відкрийте explorer.solana.com/?cluster=devnet&signature=ВАШ_SIGNATURE.
- Перейдіть до розділу Logs — там відображаються всі логи програм у хронологічному порядку.
- Знайдіть останній рядок Program failed — він вказує на програму та причину відхилення.
- Перевірте розділ Instruction Results — кожна інструкція має статус Success або Failed з описом.
Що саме шукати в Explorer:
- Рядок помилки — останній Program log: перед Program failed зазвичай містить людсько-читабельне повідомлення (якщо програма використовує msg! або Anchor).
- Код помилки — Custom program error: 0xNN для Anchor, або стандартні коди InstructionError для системних програм.
- Compute Units — порівняйте Consumed з Requested щоб зрозуміти, чи було перевищено бюджет.
- Balance Changes — перевірте, чи змінилися баланси очікуваним чином (для успішних транзакцій) або чи не було спроби списати більше, ніж є (для InsufficientFunds).
Наступний логічний крок: після того, як ви ідентифікували помилку через Explorer або локальні логи, поверніться до коду програми або клієнта, виправте конкретний параметр, і повторіть відправку на Devnet. Системне використання цього циклу — відправити, побачити помилку в логах, виправити — формує стабільну ментальну модель того, як транзакції виконуються в Solana.