{"record":{"id":"5857c6674cfd4f61","repo":"linera-io/linera-protocol","slug":"transaction-tx-hash-reverted-receipt-status-0","errorCode":null,"errorMessage":"transaction {tx_hash} reverted (receipt status 0)","messagePattern":"transaction (.+?) reverted \\(receipt status 0\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"linera-bridge/src/relay/evm.rs","lineNumber":84,"sourceCode":"    /// Waits for a transaction receipt by polling `eth_getTransactionReceipt`\n    /// directly, instead of `PendingTransactionBuilder::get_receipt()`.\n    /// `get_receipt()` starts alloy's block heartbeat, which — while any tx is\n    /// pending — backfills one `eth_getBlockByNumber` per block (see\n    /// alloy-provider `blocks.rs`). Polling the receipt hash makes zero\n    /// `getBlockByNumber` and one cheap call per interval, only while a tx is in\n    /// flight (nothing when idle). Returns an error if the tx reverted (status\n    /// 0), so a failed settlement is retried rather than treated as confirmed.\n    async fn await_receipt(&self, tx_hash: B256) -> Result<TransactionReceipt> {\n        let deadline = Instant::now() + RECEIPT_TIMEOUT;\n        loop {\n            if let Some(receipt) = self\n                .provider\n                .get_transaction_receipt(tx_hash)\n                .await\n                .context(\"eth_getTransactionReceipt failed\")?\n            {\n                if !receipt.status() {\n                    anyhow::bail!(\"transaction {tx_hash} reverted (receipt status 0)\");\n                }\n                return Ok(receipt);\n            }\n            if Instant::now() >= deadline {\n                anyhow::bail!(\"transaction {tx_hash} not confirmed within {RECEIPT_TIMEOUT:?}\",);\n            }\n            tokio::time::sleep(self.receipt_poll_interval).await;\n        }\n    }\n\n    /// Returns the FungibleBridge contract address.\n    pub fn bridge_addr(&self) -> Address {\n        self.bridge_addr\n    }\n\n    /// Returns the EVM chain's latest block number.\n    pub async fn get_block_number(&self) -> Result<u64> {\n        Ok(self.provider.get_block_number().await?)","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-bridge/src/relay/evm.rs#L66-L102","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif !receipt.status() {\n    anyhow::bail!(\"transaction {tx_hash} reverted (receipt status 0)\");\n}\n\n// after: surface gas usage + revert context so the retry path can act\nif !receipt.status() {\n    anyhow::bail!(\n        \"transaction {tx_hash} reverted (status 0, gas used {}/{}); \\\n         decode via eth_call with same calldata to get the revert reason\",\n        receipt.gas_used, receipt.gas_limit\n    );\n}","handlingStrategy":"try-catch","validationCode":"// Pre-flight the call without spending gas: eth_call the same calldata;\n// if it reverts locally, fix inputs/proofs before sending.\nlet result = bridge.register_block(&proof).call().await; // alloy eth_call\nanyhow::ensure!(result.is_ok(), \"register_block would revert: {:?}\", result.err());","typeGuard":null,"tryCatchPattern":"match evm_client.await_receipt(tx_hash).await {\n    Ok(r) => r,\n    Err(e) if e.to_string().contains(\"reverted\") => {\n        // NOT retryable as-is: decode the revert reason via eth_call/debug_traceTransaction,\n        // fix the proof/authorization, then re-send a new tx.\n        tracing::error!(%tx_hash, \"settlement reverted: {e}\");\n        return Err(e);\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["linera-bridge","evm","transaction-reverted","relay","rust","web3"],"backgroundTag":"transaction-reverted","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}