Помилка blockhash expired означає, що транзакція посилалася на blockhash, який уже вийшов із валідного вікна мережі, і валідатор відхилив її. Нижче описано, чому це відбувається, як безпечно отримувати свіжий blockhash, які патерни повторної відправки працюють у production, і коли ця помилка насправді вказує на глибшу проблему.
Причина помилки: життєвий цикл blockhash
Кожна транзакція в Solana містить поле recentBlockhash — хеш попереднього блоку. Це не просто технічна деталь: мережа використовує його для двох цілей.
По-перше, deduplication. Валідатор відкидає транзакцію з тим самим blockhash і тією самою підписом як дублікат. По-друге, захист від replay-атак: після закінчення вікна валідності blockhash транзакцію неможливо повторно відправити.
Вікно валідності становить приблизно 150 блоків. За номінального часу слота (~400 мс) це близько 60 секунд. Фактичний час може коливатися залежно від навантаження на мережу та пропускної здатності поточного кластера. Після того як блок із зазначеним blockhash виходить за межі цього вікна, будь-яка транзакція, що на нього посилається, отримує статус blockhash expired.
Ключовий наслідок: ви не можете «продовжити життя» blockhash. Єдиний спосіб відновити транзакцію — замінити blockhash на свіжий і підписати її заново.
Автоматичне отримання свіжого blockhash
Найпростіший спосіб уникнути помилки — запитувати актуальний blockhash безпосередньо перед відправкою. У стандартному клієнті @solana/web3.js для цього служить метод getLatestBlockhash.
Метод приймає параметр commitment, який визначає, наскільки «свіжим» має бути blockhash. Для більшості сценаріїв у production підходить confirmed — це баланс між швидкістю відповіді та гарантією того, що blockhash не належить orphan-блоку.
Крім того, метод sendTransaction у web3.js підтримує опцію skipPreflight і, що важливіше в цьому контексті, автоматичну підстановку blockhash через внутрішній механізм клієнта. Проте покладатися виключно на автоматику без розуміння життєвого циклу — ризикована практика: якщо між отриманням blockhash і фактичною відправкою проходить затримка (наприклад, користувач підтверджує дію в UI), транзакція все одно може не встигнути.
Рекомендація: формуйте транзакцію і відправляйте її в межах одного логічного кроку без довгих пауз. Якщо ваш потік передбачає затримку (підписання зовнішнім гаманцем, багатокрокове підтвердження) — отримуйте blockhash на фінальному етапі, безпосередньо перед підписанням.
Що перевірити після інтеграції
- Чи використовуєте ви getLatestBlockhash з commitment confirmed або вище.
- Чи немає затримки між викликом getLatestBlockhash і sendTransaction у вашому коді.
- Чи не кешуєте ви blockhash між різними транзакціями користувача.
Патерни повторної відправки транзакцій
Навіть за правильної інтеграції бувають сценарії, коли транзакція не потрапляє в блок у межах вікна валідності: мережевий збій на стороні клієнта, перевантаження RPC-вузла, тимчасова конгестія кластера. У таких випадках потрібен патерн повторної відправки.
Простий retry із новим blockhash
Найпоширеніший підхід: перехопити помилку blockhash expired, отримати новий blockhash, повторно підписати транзакцію та відправити її. У цьому патерні важливо дотримуватися кількох правил.
- Максимальна кількість спроб. Обмежте retry (зазвичай 2–3 спроби достатньо). Безлімітний retry у production створює навантаження на RPC і може маскувати справжню проблему.
- Ідентифікація дублікатів. Перш ніж повторно відправляти, перевірте статус попередньої відправки через getSignatureStatuses. Якщо транзакція вже підтверджена, повторна відправка не потрібна.
- Експоненційна затримка. Не відправляйте повторно миттєво. Затримка в 500 мс–1 с між спробами зменшує навантаження і дає мережі час на обробку черги.
Durable nonce для довгоживучих транзакцій
Для сценаріїв, де транзакція має бути валідною довго (години або дні), Solana пропонує механізм durable nonce. Замість recentBlockhash використовується спеціальний акаунт nonce, який утримує валідність транзакції необмежено часом, поки її не буде включено в блок.
Обмеження: durable nonce вимагає окремого акаунта (з відповідною оплатою rent-exempt мінімуму), додаткового підпису (nonce authority) і більш складної логіки формування транзакції. Використовуйте його лише тоді, коли стандартний retry із новим blockhash принципово не підходить — наприклад, для офчейн-підписаних транзакцій або багатосторонніх схем, де зібрати всі підписи в межах 60 секунд неможливо.
План відкату
Якщо після впровадження retry-патерну ви помічаєте сплеск blockhash expired у логах:
- Тимчасово збільште затримку між спробами до 2 секунд.
- Перевірте, чи не повертає ваш RPC-провайдер застарілий blockhash (порівняйте з іншим endpoint).
- Якщо проблема зберігається, відкотіть до простої відправки без retry і зберігайте невідправлені транзакції в чергу для ручної або відкладеної обробки.
Коли blockhash expired вказує на іншу проблему
Іноді blockhash expired — це симптом, а не причина. Ось типові ситуації, коли варто шукати глибше.
Застарілий RPC-вузол. Якщо ваш RPC-провайдер повертає blockhash, який уже близький до виходу з вікна валідності (або взагалі поза ним), транзакція може expire навіть при миттєвій відправці. Перевірте: порівняйте slot останнього блоку, який повертає ваш endpoint, із актуальним slot через незалежне джерело. Різниця понад 10–20 слотів — привід змінити провайдера або налаштувати фейловер на інший вузол.
Транзакція вже оброблена. Якщо ви отримали blockhash expired після кількох спроб retry, є ймовірність, що перша відправка насправді пройшла, але ви не отримали підтвердження через мережеву втрату. Повторна відправка з тим самим підписом, але новим blockhash створює нову транзакцію — це може призвести до подвійного виконання, якщо логіка програми цього не передбачає. Обов'язково викликайте getSignatureStatuses перед retry.
Конгестія кластера та низький пріоритет. Під час високого навантаження транзакції з низьким compute budget або без priority fee можуть затримуватися в мемпулі довше за вікно валідності blockhash. Якщо ви спостерігаєте системні blockhash expired під час сплесків активності в мережі — перевірте налаштування priority fee та compute unit limit. Це окрема тема від діагностики самої помилки, але без її вирішення retry з новим blockhash дасть лише тимчасовий ефект.
Pre-flight симуляція затримує відправку. Якщо ви не вимкнули pre-flight перевірку (опція skipPreflight: false, що є типово), клієнт спочатку симулює транзакцію. Під час конгестії симуляція може займати значний час, і до моменту фактичної відправки blockhash вже може бути близький до expiry. Для критичних шляхів, де ви впевнені в коректності транзакції, розгляньте skipPreflight: true — але за умови, що у вас є окремий механізм перевірки через simulateTransaction або інструменти моніторингу.
Наступний крок. Якщо ви впевнені, що blockhash свіжий, а помилка повторюється — перейдіть до загальної діагностики transaction failed у Solana, де розглядаються ширші причини відхилень транзакцій на рівні програми та кластера.