{"record":{"id":"49b17b862e7306eb","repo":"linera-io/linera-protocol","slug":"failed-to-decode-receipt-logs","errorCode":null,"errorMessage":"failed to decode receipt logs","messagePattern":"failed to decode receipt logs","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"linera-bridge/contracts/evm-bridge/src/contract.rs","lineNumber":228,"sourceCode":"            .state\n            .verified_block_hashes\n            .contains(&block_hash.0)\n            .await\n            .expect(\"failed to check verified block hashes\")\n        {\n            self.verify_block_hash(block_hash.0).await;\n        }\n\n        // 2. Verify receipt inclusion via MPT proof\n        let proof_bytes: Vec<Bytes> = proof_nodes\n            .iter()\n            .map(|n| Bytes::copy_from_slice(n))\n            .collect();\n        proof::verify_receipt_inclusion(receipts_root, tx_index, receipt_rlp, &proof_bytes)\n            .expect(\"receipt inclusion proof failed\");\n\n        // 3. Decode receipt logs and parse the deposit event\n        let logs = proof::decode_receipt_logs(receipt_rlp).expect(\"failed to decode receipt logs\");\n        // `log_index` is a u64 but indexes a Vec (usize). On wasm32 `usize` is\n        // 32-bit, so an unchecked `as usize` cast would truncate — letting\n        // `log_index` and `log_index + 2^32` select the same log while hashing\n        // to different `DepositKey`s (replay-guard bypass → double mint). A\n        // checked cast rejects any value that does not fit `usize`; the full\n        // u64 is preserved for the `DepositKey` below.\n        let log_index_usize = usize::try_from(log_index).expect(\"log_index out of range\");\n        assert!(\n            log_index_usize < logs.len(),\n            \"log_index {} out of range (receipt has {} logs)\",\n            log_index,\n            logs.len()\n        );\n        let bridge_contract_bytes =\n            self.state.bridge_contract_address.get().expect(\n                \"bridge contract address not registered — call RegisterFungibleBridge first\",\n            );\n        let bridge_contract = alloy_primitives::Address::from(bridge_contract_bytes);","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-bridge/contracts/evm-bridge/src/contract.rs#L210-L246","documentation":"Step 3 of process_deposit decodes the submitted receipt_rlp into its list of logs with proof::decode_receipt_logs and expects success. The panic means the bytes passed the earlier inclusion proof as opaque data but do not deserialize as a receipt whose logs field is a well-formed list — usually non-canonical or mismatched RLP structure for the log entries. The transaction aborts without minting.","triggerScenarios":"The relayer submits bytes that hash correctly under the MPT proof but were re-encoded with a different receipt schema (wrong field count/types after an EIP changed the receipt format); manual construction of receipt_rlp instead of using RPC-returned bytes; alloy version skew between the relayer's encoder and the contract's decoder.","commonSituations":"Relayers that store receipts in a database and re-serialize them; chain-fork or EIP-transition windows where receipt types differ (legacy vs typed receipts, EIP-1559/4844); test harnesses with handcrafted receipt fixtures.","solutions":["Submit the exact receipt bytes from eth_getTransactionReceipt without re-encoding","If you build receipts yourself, round-trip them through the same alloy version the contract uses and assert decode_receipt_logs succeeds locally","For typed receipts, ensure the type byte is preserved as the first RLP list element","Pin alloy versions between relayer and evm-bridge contract and add a local decode check to CI fixtures"],"exampleFix":"// before\nlet receipt_rlp = my_db_reencoded_receipt(); // hashes match proof but decodes wrong\nsubmit(ProcessDeposit { receipt_rlp, .. });\n\n// after\nlet receipt_hex = rpc.get_transaction_receipt(tx_hash).await?.unwrap().raw; // exact bytes\nlet receipt_rlp = hex::decode(receipt_hex.trim_start_matches(\"0x\"))?;\ndebug_assert!(proof::decode_receipt_logs(&receipt_rlp).is_ok()); // preflight\nsubmit(ProcessDeposit { receipt_rlp, .. });","handlingStrategy":"validation","validationCode":"// Confirm the receipt decodes to logs before paying for the transaction:\nif proof::decode_receipt_logs(&receipt_rlp).is_err() {\n    tracing::warn!(\"receipt RLP will fail contract decode — skipping\");\n    return;\n}\nsubmit(ProcessDeposit { receipt_rlp, .. });","typeGuard":"fn receipt_has_decodable_logs(receipt_rlp: &[u8]) -> bool {\n    proof::decode_receipt_logs(receipt_rlp).is_ok()\n}","tryCatchPattern":"// Local pre-decode with the same alloy version the contract uses:\nlet logs = proof::decode_receipt_logs(&receipt_rlp)\n    .unwrap_or_else(|e| panic!(\"relayer produced undecodable receipt: {e}\")); // fail the pipeline, not the chain","preventionTips":["Pass through RPC receipt bytes unmodified — no database re-serialization","Pin alloy versions between relayer and contract","Cover legacy vs typed receipt encodings in relayer unit tests"],"tags":["linera","bridge","ethereum","rlp","receipt","decode","panic"],"backgroundTag":"rlp-decode-failed","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}