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
- 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.
- Verify the relayer address is authorized on the contract and matches the account sending the tx.
- If the failure is a stale/duplicate block registration, re-fetch current LightClient state (epoch, latest block) and rebuild the proof before re-sending.
- Check contract addresses and ABI versions in relay config against the deployed contracts; redeploy or reconfigure on mismatch.
- 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
- eth_call every settlement calldata before sending so reverts are caught pre-gas.
- Verify the relayer address is authorized on FungibleBridge/LightClient at startup.
- Keep contract ABIs and deployed bytecode in lockstep with the relay binary version.
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
- transaction {tx_hash} not confirmed within {RECEIPT_TIMEOUT:
- 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/5857c6674cfd4f61.
Report an issue: GitHub.