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

  1. 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.
  2. Raise the gas price / priority fee the relayer attaches to settlement txs so they confirm within 180s.
  3. Switch to a reliable RPC provider (paid endpoint or own node) if receipts are intermittently unavailable.
  4. 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

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

Related errors


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/55b22dab55508e51. Report an issue: GitHub.