{"record":{"id":"2e7ce19be71ca95a","repo":"linera-io/linera-protocol","slug":"receipt-inclusion-proof-failed","errorCode":null,"errorMessage":"receipt inclusion proof failed","messagePattern":"receipt inclusion proof failed","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"linera-bridge/contracts/evm-bridge/src/contract.rs","lineNumber":225,"sourceCode":"        if self.state.rpc_endpoint.get().is_empty() {\n            log::warn!(\"rpc_endpoint is empty — skipping block finality verification.\");\n        } else if !self\n            .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(","sourceCodeStart":207,"sourceCodeEnd":243,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-bridge/contracts/evm-bridge/src/contract.rs#L207-L243","documentation":"Step 2 of process_deposit verifies a Merkle-Patricia trie inclusion proof: proof_nodes must prove that receipt_rlp at tx_index is included under the receipts_root decoded from the block header. The panic means the MPT proof did not verify — wrong node set, wrong tx_index, tampered receipt bytes, or a proof generated against a different block's receipts root. The transaction aborts and no deposit is minted.","triggerScenarios":"Relayer submits proof_nodes for the wrong transaction index or a different block; receipt_rlp is re-encoded non-canonically so its hash no longer matches the leaf in the proof; nodes are missing (truncated proof array) or in reversed order; the header belongs to a different block than the receipts.","commonSituations":"Relayers that assemble proofs from eth_getProof or manual trie walks and mismatch block/tx indices after reorgs; non-canonical RLP re-encoding (e.g. integers encoded with leading zeros) when receipts are round-tripped through a store; concurrent relayers mixing proofs from competing forks.","solutions":["Verify the proof off-chain first with the same trie logic (e.g. alloy-trie / ethers MPT verifier) using the header's receiptsRoot, tx_index, and receipt_rlp you are about to submit","Ensure receipt_rlp is the exact bytes returned by the RPC (eth_getTransactionReceipt), not a re-encoding","Re-derive tx_index from the block body and confirm proof_nodes came from the same block hash","Handle reorgs: re-fetch header and proof together after finality and resubmit as one consistent set"],"exampleFix":"// before\nsubmit(ProcessDeposit { block_header_rlp, receipt_rlp, proof_nodes, tx_index, log_index });\n// transaction aborts: receipt inclusion proof failed\n\n// after — verify MPT inclusion off-chain exactly like the contract does\nuse alloy_trie::proof::ProofVerification;\nlet root = header.receipts_root;\nlet key = alloy_rlp::encode_fixed_size(&tx_index).to_vec();\nProofVerification::new(root, key, receipt_rlp.clone(), proof_nodes.clone())\n    .verify()\n    .context(\"proof invalid — do not submit\")?;\nsubmit(ProcessDeposit { block_header_rlp, receipt_rlp, proof_nodes, tx_index, log_index });","handlingStrategy":"validation","validationCode":"// Verify MPT inclusion off-chain with the same parameters before submitting:\nuse alloy_trie::proof::ProofVerification;\nfn inclusion_proof_valid(root: B256, tx_index: u64, receipt_rlp: &[u8], nodes: &[[u8; 33]]) -> bool {\n    let key = alloy_rlp::encode(tx_index);\n    let proof: Vec<alloy_primitives::Bytes> = nodes.iter().map(|n| Bytes::copy_from_slice(n)).collect();\n    ProofVerification::new(root, key.to_vec(), receipt_rlp.to_vec(), proof).verify().is_ok()\n}","typeGuard":null,"tryCatchPattern":"// On contract rejection, the relayer should not blindly retry: re-fetch the\n// full tuple (header, receipt, proof, tx_index) from the source chain after\n// any reorg, re-verify locally, then resubmit as one consistent set.","preventionTips":["Treat header, receipt, proof, and indices as one atomic bundle fetched in a single pass","Never re-encode receipt bytes; forward RPC-returned bytes verbatim","Log the receiptsRoot/key/leaf hash of failing proofs to diff against the contract's view"],"tags":["linera","bridge","ethereum","merkle-patricia-trie","proof-verification","panic"],"backgroundTag":"merkle-proof-verification-failed","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}