Повторна відправка транзакцій на Solana вимагає чіткого розуміння, коли мережа гарантує ідемпотентність, а коли ваша логіка може виконатися двічі. Нижче — інженерний підхід до retries, який мінімізує ризики подвійного списання та витрачає RPC-ліміти раціонально.
Коли повторна відправка доречна
Не кожна помилка підлягає повторній спробі. Критерій простий: помилка має бути тимчасовою і не пов'язаною з логікою вашої програми.
Доречно retry:
- TransactionExpiredBlockhash — блокхеш застарів, але стан акаунтів не змінився. Треба отримати свіжий
recentBlockhashі підписати нову транзакцію. - Таймаут з'єднання з RPC-вузлом або відсутність відповіді (HTTP 502, 503, 504).
- RPC rate limiting (HTTP 429) — після відповідної затримки (backoff).
- BlockhashNotFound — RPC-вузол ще не бачив цей блокхеш. Можна повторити через короткий інтервал або запросити інший блокхеш.
Не доречно retry без зміни транзакції:
- Будь-який InstructionError — помилка виконання інструкції означає, що логіка програми відхилила операцію. Повторна відправка з тим самим станом дасть той самий результат.
- InsufficientFundsForFee — якщо баланс не поповнено між спробами.
- PrecheckFailed з причинами на кшталт InvalidAccountIndex або CallChainTooDeep — це структурні помилки транзакції, які не зникнуть від повторної відправки.
- Помилки валідації підпису — якщо підпис сформовано некоректно, повторна відправка без перепідписання безглузда.
Межа між «доречно» і «не доречно» не завжди однозначна. Наприклад, AccountInUse може свідчити про тимчасове блокування акаунта іншою транзакцією в поточному слоті — у цьому випадку коротка затримка і повторна спроба виправдані. Але та сама помилка може бути ознакою логічної гонки у вашому застосунку, яку retry лише маскує.
Ідемпотентність транзакцій
Solana гарантує ідемпотентність на рівні мережі: транзакція з одним і тим самим підписом обробляється лише один раз. Якщо ви відправляєте ідентичну транзакцію повторно (той самий підпис, той самий блокхеш), RPC поверне результат оригінальної відправки — успішний чи ні.
Проте ця гарантія руйнується в двох сценаріях:
1. Оновлення блокхеша. Коли блокхеш застарів (приблизно 60 секунд, але під час конгестії інтервал може скорочуватися), ви змушені створювати нову транзакцію з новим блокхешем і новим підписом. Для мережі це інша транзакція. Якщо оригінальна насправді підтверджена, але ви цього не дізналися (наприклад, через втрату відповіді RPC), нова відправка виконає вашу логіку вдруге.
2. Зміна порядку інструкцій або даних. Будь-яка модифікація тіла транзакції змінює хеш повідомлення, що вимагає нового підпису і робить транзакцію новою для мережі.
Практичний захист на рівні програми:
- Використовуйте PDA-рахунки як слоти для ідемпотентності. Перед виконанням основної логіки інструкція перевіряє спеціальний акаунт-прапорець: якщо він уже ініціалізовано — повертає помилку або мовчазний успіх.
- Зберігайте унікальний ідентифікатор операції (operation ID) у стані програми. Перед виконанням перевіряйте, чи не оброблявся цей ID раніше.
- Для фінансових операцій розгляньте двофазний підхід: спочатку зарезервуйте кошти окремою інструкцією, потім завершіть переказ. Якщо друга транзакція загубиться, резервування можна скасувати таймаутом.
Ці механізми додають складності та витрати на rent, але є єдиним надійним способом захисту від подвійного виконання при retries з новим блокхешем.
Обробка duplicate signature
Коли ви відправляєте транзакцію з підписом, який мережа вже бачила, RPC-вузол не відхиляє її одразу. Поведінка залежить від стану обробки:
- Транзакція в mempool або обробляється. RPC поверне статус, що транзакція вже відома. Це нормальна ситуація при агресивних retries — не сприймайте це як помилку.
- Транзакція підтверджена. RPC поверне результат оригінального виконання разом із сигнатурою. Ваш код має розпізнати цей кейс і не інтерпретувати його як новий успіх.
- Транзакція відхилена. RPC поверне оригінальну помилку. Повторна відправка з тим самим підписом не змінить результат.
Типова помилка — інтерпретувати відповідь з існуючою сигнатурою як «щось зламалося» і починати створювати нову транзакцію. Навпаки: наявність відомої сигнатури — це позитивний сигнал, що ваша транзакція не загубилася.
Рекомендована послідовність у retry-циклі:
- Зберігайте відправлену сигнатуру в локальному сховищі (пам'ять, Redis, БД) з прив'язкою до вашої бізнес-операції.
- При повторній спробі спочатку викличте
getSignatureStatusesдля збереженої сигнатури. - Якщо статус — confirmationStatus рівня
confirmedабоfinalized, припиніть retries і поверніть результат. - Якщо статус відсутній або
finalizedз помилкою — оцініть, чи доречна нова транзакція згідно з критеріями з першого розділу.
Цей підхід зменшує навантаження на RPC і виключає випадки, коли ви генеруєте нову транзакцію, тоді як стара вже в процесі підтвердження.
Моніторинг ефективності retries
Retries без моніторингу — це прихована лімітна бомба. Кожна повторна відправка споживає RPC-квоти, а при використанні платних провайдерів — безпосередньо гроші. Щоб контролювати ситуацію, відстежуйте такі метрики:
| Метрика | Що показує | Де рахувати |
|---|---|---|
| Retry rate (спроби / загальна кількість транзакцій) | Частка транзакцій, що потребують повторної відправки | Клієнтський рівень, перед sendTransaction |
| Success after N retries | Розподіл успішних підтверджень за номером спроби | Клієнтський рівень, після confirmTransaction |
| Retry latency | Час від першої спроби до фінального результату | Клієнтський рівень, end-to-end |
| Error breakdown by type | Які саме помилки викликають retries | Клієнтський рівень, парсинг RPC-відповідей |
| Duplicate signature rate | Частка повторних відправок, де сигнатура вже була відома мережі | Клієнтський рівень, відповідь RPC |
Орієнтовні порогові значення для алертів:
- Retry rate вище 10–15% протягом 5 хвилин — сигнал про проблеми з RPC-провайдером або мережевою конгестією. Не змінюйте пороги без попереднього аналізу вашого нормального профіля: для деяких типів транзакцій (наприклад, з конкуренцією за PDA-рахунки) вищий retry rate може бути нормою.
- Зростання частки TransactionExpiredBlockhash серед причин retry — може свідчити про уповільнення підтверджень або про те, що ваш
commitmentрівень завищений для поточної ситуації. - Duplicate signature rate вище 30% — ваш retry-цикл надто агресивний і витрачає квоти на відправку транзакцій, які вже обробляються.
Корисно корелювати ці метрики з зовнішніми сигналами: завантаженням мережі (TPS, кількість невиконаних транзакцій у mempool) та статусом вашого RPC-провайдера. Якщо retry rate стрибає одночасно з падінням відповідей RPC, проблема скоріше інфраструктурна, а не у вашій логіці.
Практичне застосування метрик: якщо ви бачите, що більшість успішних транзакцій підтверджується з першої або другої спроби, а третя і наступні майже ніколи не дають результату — обмежте кількість retries двома. Це зменшить навантаження без втрати ефективності. Навпаки, якщо є стабільна частка успіхів на третій-четвертій спробі (типово для AccountInUse під високим навантаженням), збільшення ліміту може бути виправданим.
Усі метрики повинні бути прив'язані до конкретного типу транзакції у вашому застосунку. Агреговані показники по всім операціях розмивають картину: retry-профіль токен-переказів відрізняється від retry-профілю інструкцій із складною CPI-ланцюжкою.