Помилка 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, не відправляйте її сліпо повторно. Спочатку перевірте:

  1. Чи не була попередня спроба насправді оброблена успішно (перевірте підпис через getSignatureStatuses).
  2. Чи не змінився стан акаунтів таким чином, що транзакцію треба перезібрати (наприклад, змінився nonce або баланс).
  3. Чи не минув термін дії 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. Це знизить пропускну здатність, але гарантовано усуне конфлікти блокування, даючи час на діагностику без втрати коштів користувачів. Після виявлення кореневої причини поверніть паралельну відправку з відповідними патернами уникнення.

Джерела