Помилка AccountInUse виникає, коли runtime Solana відхиляє транзакцію, оскільки принаймні один акаунт із її списку вже заблокований іншою транзакцією в межах того самого слота обробки. Це не помилка логіки програми — це помилка планування транзакцій на рівні кластера. Нижче наведено алгоритм діагностики від першого прояву до кореневої причини.
Причина: одночасний запис до одного акаунта
Solana обробляє транзакції пакетами в межах одного слота. Перед виконанням runtime блокує всі акаунти, які транзакція збирається модифікувати (зазначені як writable). Якщо дві транзакції в одному пакеті або в паралельних потоках обробки намагаються записати в один і той самий акаунт, друга отримує AccountInUse.
Ключові моменти, які треба розуміти перед діагностикою:
- Блокування застосовується лише до writable-акаунтів. Акаунти, передані як readonly, не викликають конфлікту.
- Блокування триває протягом обробки транзакції в межах слота, а не до фіналізації (commitment).
- Помилка повертається до відправника ще до виклику будь-якої програми — лог вашого Anchor- або Rust-коду не виконається.
- Це не те саме, що AccountLocked (помилка 0x0), яка стосується стану акаунта після попередньої транзакції.
Виявлення конфліктуючих транзакцій
Діагностика зводиться до того, щоб знайти, яка саме транзакція утримує блокування і чому ваші транзакції потрапляють в один слот.
Крок 1. Отримайте деталі відхиленої транзакції
Викличте getTransaction з підписом відхиленої транзакції та параметром commitment: confirmed (або вище). У відповіді перевірте:
- slot — слот, у якому транзакція оброблялася.
- meta.err — має містити
AccountInUse. - transaction.message.accountKeys — повний список акаунтів транзакції.
Крок 2. Знайдіть інші транзакції в тому самому слоті
Для кожного writable-акаунта з відхиленої транзакції викличте getSignaturesForAddress з обмеженням за часом навколо слота помилки. Звертайте увагу на транзакції, що потрапили в той самий слот або сусідні слоти з перекриттям обробки.
Альтернативно, якщо ви використовуєте власний RPC-вузол або маєте доступ до логів валідатора, знайдіть запис у agave-validator з рівнем логування DEBUG або TRACE, де вказано конфліктуючі підписи.
Крок 3. Перевірте спільні writable-акаунти
Порівняйте списки writable-акаунтів вашої відхиленої транзакції та знайденої конфліктуючої. Спільний writable-акаунт — це джерело проблеми. Типові кандидати:
- Акаунт-стану програми (особливо якщо він єдиний для всіх користувачів).
- PDA-акаунт, який обчислюється однаково для різних операцій.
- Акаунт кошелька відправника, якщо кілька транзакцій списують з нього одночасно.
- Акаунт ліквідності AMM або пулу, якщо ви працюєте з DeFi-протоколами.
Крок 4. Проаналізуйте джерело відправки
Перевірте ваш бекенд або клієнтський код на предмет:
- Паралельної відправки транзакцій без очікування підтвердження попередньої.
- Неконтрольованого повторного відправлення (retry) після таймауту без перевірки статусу першої спроби.
- Батчингу транзакцій, де залежності між ними не враховані.
Патерни уникнення AccountInUse
Послідовна відправка з підтвердженням
Найпростіший і найнадійніший підхід: відправляйте наступну транзакцію лише після того, як попередня досягла бажаного рівня commitment. Використовуйте confirmTransaction з commitment: confirmed або finalized залежно від вимог вашого застосунку.
Обмеження: цей патерн зменшує пропускну здатність, оскільки кожна транзакція чекає підтвердження. Для високонавантажених систем це може бути неприпустимо.
Розділення writable-акаунтів
Проєктуйте архітектуру акаунтів так, щоб паралельні операції різних користувачів або сесій не перетиналися на спільних writable-акаунтах:
- Замість єдиного акаунта-стану програми використовуйте індивідуальні PDA для кожного користувача чи сесії.
- Якщо спільний акаунт неминучий (наприклад, глобальний конфіг), читайте його як readonly у всіх транзакціях, крім тих, що дійсно його модифікують.
- Для агрегованих даних розгляньте патерн з кількома «відра́ми» (buckets), де кожна паралельна операція працює з власним відром.
Топологічне сортування в батчах
Якщо ви відправляєте транзакції батчами, побудуйте граф залежностей за writable-акаунтами і відсортуйте транзакції так, щоб транзакції, що пишуть в один акаунт, не опинялися в одному пакеті. Це не гарантує відсутність конфліктів (бо порядок обробки на валідаторі може відрізнятися), але суттєво знижує їхню ймовірність.
Ідемпотентний retry із перевіркою стану
Якщо транзакція відхилена з AccountInUse, не відправляйте її сліпо повторно. Спочатку перевірте:
- Чи не була попередня спроба насправді оброблена успішно (перевірте підпис через getSignatureStatuses).
- Чи не змінився стан акаунтів таким чином, що транзакцію треба перезібрати (наприклад, змінився nonce або баланс).
- Чи не минув термін дії
recent_blockhash— у такому разі транзакцію треба пересобрати з новим blockhash.
Коли AccountInUse є симптомом іншої проблеми
Іноді AccountInUse не є первинною проблемою, а маскує або сигналізує про інші дефекти в архітектурі чи логіці.
MEV-конкуренція за спільні ресурси
Якщо ваш застосунок взаємодіє з популярними пулами ліквідності або DEX-ами, AccountInUse може бути наслідком того, що MEV-боти та інші користувачі масово відправляють транзакції в той самий акаунт пулу. У цьому випадку проблема не у вашому коді, а в тому, що ви намагаєтеся записати в «гарячий» акаунт без достатнього пріоритетизації (priority fees).
Діагностика: перевірте, чи є конфліктуючий акаунт публічним пулом. Якщо так — розгляньте використання compute budget instructions для підвищення пріоритету або архітектурне рішення, що уникає прямого запису в цей акаунт.
Некоректна логіка retry у бекенді
Якщо ваш бекенд отримує таймаут від RPC і одразу відправляє ту саму транзакцію повторно, перша копія може все ще оброблятися. Друга копія отримає AccountInUse. При цьому перша може як успішно завершитися, так і відхилитися з іншою помилкою.
Діагностика: перевірте логи бекенда на наявність дублікатів підписів, відправлених з інтервалом менше ніж 200–400 мс. Якщо дублікати є — виправте логіку retry: спочатку запитайте статус підпису, і лише якщо статус відсутній, відправляйте повторно.
Неочікуваний writable-акаунт у CPI-ланцюзі
Ваша транзакція може неявно модифікувати акаунт через CPI (Cross-Program Invocation). Наприклад, ви передаєте акаунт як readonly у свою програму, але вона передає його як writable у виклик сторонньої програми. Це створює блокування, яке ви не очікували.
Діагностика: перевірте всі CPI-виклики у вашій програмі та переконайтеся, що флаг writable не змінюється на проміжних етапах. Використовуйте simulateTransaction з увімкненим replaceRecentBlockhash: false, щоб побачити повний ланцюг викликів без фактичного відправлення.
Гонитва умов у розподілених сервісах
Якщо кілька екземплярів вашого бекенда (наприклад, кілька под у Kubernetes) одночасно обробляють запити від одного користувача, вони можуть відправити транзакції, що пишуть в одні й ті самі акаунти. AccountInUse тут — симптом відсутності координації між екземплярами.
Діагностика: перевірте, чи маєте ви механізм блокування на рівні бекенда (distributed lock) для операцій, що зачіпають спільні writable-акаунти одного користувача. Якщо немає — це коренева причина.
План відкату під час діагностики
Якщо AccountInUse починає масово з'являтися в production і ви ще не знайшли причину, тимчасово переключіть відправку транзакцій у послідовний режим з commitment: finalized. Це знизить пропускну здатність, але гарантовано усуне конфлікти блокування, даючи час на діагностику без втрати коштів користувачів. Після виявлення кореневої причини поверніть паралельну відправку з відповідними патернами уникнення.