linera-io/linera-protocol · error
transaction {tx_hash} not confirmed within {RECEIPT_TIMEOUT:
Error message
transaction {tx_hash} not confirmed within {RECEIPT_TIMEOUT:?} What it means
await_receipt polled eth_getTransactionReceipt for a full RECEIPT_TIMEOUT (180s) and never saw the transaction mined. The tx may still be pending in the mempool (gas price too low for current congestion), dropped by the RPC provider, or the RPC endpoint is unhealthy. The timeout is deliberately generous so a slow-but-valid tx is not re-sent; the monitor's retry path re-sends after this error.
Source
Thrown at linera-bridge/src/relay/evm.rs:89
/// `getBlockByNumber` and one cheap call per interval, only while a tx is in
/// flight (nothing when idle). Returns an error if the tx reverted (status
/// 0), so a failed settlement is retried rather than treated as confirmed.
async fn await_receipt(&self, tx_hash: B256) -> Result<TransactionReceipt> {
let deadline = Instant::now() + RECEIPT_TIMEOUT;
loop {
if let Some(receipt) = self
.provider
.get_transaction_receipt(tx_hash)
.await
.context("eth_getTransactionReceipt failed")?
{
if !receipt.status() {
anyhow::bail!("transaction {tx_hash} reverted (receipt status 0)");
}
return Ok(receipt);
}
if Instant::now() >= deadline {
anyhow::bail!("transaction {tx_hash} not confirmed within {RECEIPT_TIMEOUT:?}",);
}
tokio::time::sleep(self.receipt_poll_interval).await;
}
}
/// Returns the FungibleBridge contract address.
pub fn bridge_addr(&self) -> Address {
self.bridge_addr
}
/// Returns the EVM chain's latest block number.
pub async fn get_block_number(&self) -> Result<u64> {
Ok(self.provider.get_block_number().await?)
}
/// Returns the relayer's ETH balance in wei.
pub async fn get_relayer_balance(&self) -> Result<U256> {
Ok(self.provider.get_balance(self.relayer_addr).await?)View on GitHub (pinned to 6c226ddcb3)
Solutions
- Let the monitor's retry path re-send (that is the designed behavior) — but first confirm the old tx is not still pending to avoid duplicates.
- Raise the gas price / priority fee the relayer attaches to settlement txs so they confirm within 180s.
- Switch to a reliable RPC provider (paid endpoint or own node) if receipts are intermittently unavailable.
- If congestion is chronic, increase RECEIPT_TIMEOUT and/or shorten receipt_poll_interval in configuration.
Example fix
// before
if Instant::now() >= deadline {
anyhow::bail!("transaction {tx_hash} not confirmed within {RECEIPT_TIMEOUT:?}");
}
// after (caller): treat as retryable, check pending state before re-sending
match evm_client.await_receipt(tx_hash).await {
Ok(receipt) => { /* ... */ }
Err(e) if e.to_string().contains("not confirmed within") => {
tracing::warn!(%tx_hash, %e, "confirmation timeout; monitor retry will re-send");
// return the error so the retry loop owns re-submission
return Err(e);
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: retry
Validate before calling
// Before sending, sanity-check the provider and gas market: let block = provider.get_block_number().await?; // provider alive // ensure max_fee_per_gas >= current base fee * 1.2 so the tx is mineable within 180s.
Try / catch
match evm_client.await_receipt(tx_hash).await {
Ok(r) => r,
Err(e) if e.to_string().contains("not confirmed within") => {
// Retryable: confirm the old tx is not pending (eth_getTransactionByHash),
// then re-send with a higher fee via the monitor's retry path.
tracing::warn!(%tx_hash, "{e}");
return Err(e); // monitor retry loop re-sends
}
Err(e) => return Err(e),
} Prevention
- Attach competitive EIP-1559 fees (base fee + margin) so settlement confirms well under 180s.
- Use a dependable RPC endpoint; alert when receipt polling errors repeatedly.
- Before re-sending after a timeout, check whether the original tx is still pending to avoid double-settling.
When it happens
Trigger: Sending register_block/process_burns/add_committee txs with a gas price below what the current base fee demands during congestion; using a rate-limited or flaky public RPC that stops returning receipts; a chain reorg dropping the tx; the tx being silently dropped from the mempool.
Common situations: Public RPC endpoints (e.g. public Base Sepolia) throttling the relayer; network congestion spikes making the configured gas price uncompetitive; relay restarted mid-wait so the pending tx hash is lost and only resurfaces via this timeout on the next attempt.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- transaction {tx_hash} reverted (receipt status 0)
- EVM scan loop exited unexpectedly: {result:?}
- Chain listener exited unexpectedly: {result:?}
- Linera scan loop exited unexpectedly: {result:?}
- Retry loop exited unexpectedly: {result:?}
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/55b22dab55508e51.
Report an issue: GitHub.