linera-io/linera-protocol · critical

transaction {tx_hash} reverted (receipt status 0)

Error message

transaction {tx_hash} reverted (receipt status 0)

What it means

The bridge relayer's await_receipt polled eth_getTransactionReceipt for a settlement transaction it had sent (register_block, process_burns, or add_committee) and got a receipt whose status flag is 0: the transaction was mined but the contract call reverted on-chain. This is a definitive on-chain failure, not a network hiccup — the transaction consumed gas and its effects were rolled back.

Source

Thrown at linera-bridge/src/relay/evm.rs:84

    /// Waits for a transaction receipt by polling `eth_getTransactionReceipt`
    /// directly, instead of `PendingTransactionBuilder::get_receipt()`.
    /// `get_receipt()` starts alloy's block heartbeat, which — while any tx is
    /// pending — backfills one `eth_getBlockByNumber` per block (see
    /// alloy-provider `blocks.rs`). Polling the receipt hash makes zero
    /// `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?)

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Decode the revert: re-send an eth_call (or use the tx input on a local fork/anvil) with the same calldata to get the revert reason from FungibleBridge/LightClient.
  2. Verify the relayer address is authorized on the contract and matches the account sending the tx.
  3. If the failure is a stale/duplicate block registration, re-fetch current LightClient state (epoch, latest block) and rebuild the proof before re-sending.
  4. Check contract addresses and ABI versions in relay config against the deployed contracts; redeploy or reconfigure on mismatch.
  5. Increase the gas limit if the revert is out-of-gas.

Example fix

// before
if !receipt.status() {
    anyhow::bail!("transaction {tx_hash} reverted (receipt status 0)");
}

// after: surface gas usage + revert context so the retry path can act
if !receipt.status() {
    anyhow::bail!(
        "transaction {tx_hash} reverted (status 0, gas used {}/{}); \
         decode via eth_call with same calldata to get the revert reason",
        receipt.gas_used, receipt.gas_limit
    );
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight the call without spending gas: eth_call the same calldata;
// if it reverts locally, fix inputs/proofs before sending.
let result = bridge.register_block(&proof).call().await; // alloy eth_call
anyhow::ensure!(result.is_ok(), "register_block would revert: {:?}", result.err());

Try / catch

match evm_client.await_receipt(tx_hash).await {
    Ok(r) => r,
    Err(e) if e.to_string().contains("reverted") => {
        // NOT retryable as-is: decode the revert reason via eth_call/debug_traceTransaction,
        // fix the proof/authorization, then re-send a new tx.
        tracing::error!(%tx_hash, "settlement reverted: {e}");
        return Err(e);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: A register_block tx where the LightClient rejects the block proof (invalid signature quorum, wrong epoch, stale height); a process_burns tx reverting in the FungibleBridge (e.g. proof or amount mismatch); add_committee reverting because the committee update is invalid or unauthorized (wrong relayer address); out-of-gas or a contract upgrade changing expected behavior.

Common situations: Relayer account is not the authorized relayer on the FungibleBridge contract; contract deployed from a different commit than the relay binary (ABI/logic mismatch); invalid or stale block proof submitted after a competing register_block already advanced the LightClient; insufficient gas limit configured for the settlement tx.

Related errors


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