{"record":{"id":"abedde0a9861bbe7","repo":"linera-io/linera-protocol","slug":"invalid-log-rlp-e","errorCode":null,"errorMessage":"invalid log RLP: {e}","messagePattern":"invalid log RLP: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"linera-bridge/src/proof/mod.rs","lineNumber":551,"sourceCode":"\n/// Skips one RLP item (string or list) by reading its header and advancing past the payload.\nfn skip_rlp_item(data: &mut &[u8]) -> Result<()> {\n    let header = alloy_rlp::Header::decode(data).map_err(|e| anyhow!(\"invalid RLP item: {e}\"))?;\n    ensure!(\n        data.len() >= header.payload_length,\n        \"not enough data to skip RLP item\"\n    );\n    *data = &data[header.payload_length..];\n    Ok(())\n}\n\n/// Decodes a single log entry from RLP.\n///\n/// Enforces the declared payload boundary: after decoding address, topics, and data,\n/// verifies that exactly `payload_length` bytes were consumed.\nfn decode_log(data: &mut &[u8]) -> Result<ReceiptLog> {\n    let log_header =\n        alloy_rlp::Header::decode(data).map_err(|e| anyhow!(\"invalid log RLP: {e}\"))?;\n    ensure!(log_header.list, \"log must be an RLP list\");\n    ensure!(\n        data.len() >= log_header.payload_length,\n        \"log payload extends past available data\"\n    );\n\n    // Limit reads to the declared payload boundary.\n    let mut log_data_buf = &data[..log_header.payload_length];\n    *data = &data[log_header.payload_length..];\n\n    let address = <Address as alloy_rlp::Decodable>::decode(&mut log_data_buf)\n        .map_err(|e| anyhow!(\"invalid log address: {e}\"))?;\n\n    // Decode topics list\n    let topics_header = alloy_rlp::Header::decode(&mut log_data_buf)\n        .map_err(|e| anyhow!(\"invalid topics list RLP: {e}\"))?;\n    ensure!(topics_header.list, \"topics must be an RLP list\");\n    ensure!(","sourceCodeStart":533,"sourceCodeEnd":569,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-bridge/src/proof/mod.rs#L533-L569","documentation":"Thrown while decoding an Ethereum receipt log entry inside a bridge deposit proof: the very first RLP header of the entry cannot be parsed. decode_log walks the receipt's log list, and each entry must start with a well-formed RLP list header. Corrupt, truncated, or misaligned bytes at the cursor position fail here before any field is read.","triggerScenarios":"decode_receipt_logs is called on receipt bytes where the cursor is not at the start of a valid RLP header: truncated proof payload, a receipt that is not canonical Ethereum receipt RLP ([status, cumulativeGasUsed, logsBloom, logs]), or an off-by-N cursor left by the preceding receipt-field decoding (status/bloom).","commonSituations":"Receipts re-encoded by a non-canonical RLP encoder; byte offsets shifted because the logsBloom or status field was skipped incorrectly; proofs fetched from an incompatible chain or hardfork; partially copied hex blobs (odd length, 0x prefix left in).","solutions":["Decode the same receipt bytes with a reference implementation (e.g. alloy_consensus::Receipt or an RPC eth_getTransactionReceipt) to confirm they are canonical receipt RLP.","Check the slicing/skipping logic that positions the cursor before the log loop (status byte, cumulativeGasUsed, logsBloom) for off-by-one errors.","Re-fetch the receipt and Merkle proof from the EVM RPC and retry verification with fresh bytes.","Confirm the proof was produced for the same chain/hardfork the light client tracks."],"exampleFix":"// before: raw bytes from the proof are handed straight to the log decoder\nlet logs = decode_receipt_logs(&receipt_bytes)?; // fails: \"invalid log RLP: ...\"\n\n// after: pre-validate the whole log with the reference decoder, reject the proof on mismatch\nuse alloy_rlp::Decodable;\nif alloy_primitives::Log::decode(&mut &log_bytes[..]).is_err() {\n    anyhow::bail!(\"rejecting receipt proof: log entry is not canonical RLP\");\n}\nlet logs = decode_receipt_logs(&receipt_bytes)?;","handlingStrategy":"validation","validationCode":"// Cheap pre-check: the reference decoder must accept the whole log entry\n// before the custom field-by-field proof parser runs.\nuse alloy_rlp::Decodable;\nfn is_canonical_log(rlp: &[u8]) -> bool {\n    alloy_primitives::Log::decode(&mut &rlp[..]).is_ok()\n}","typeGuard":"fn is_canonical_log(rlp: &[u8]) -> bool {\n    use alloy_rlp::Decodable;\n    alloy_primitives::Log::decode(&mut &rlp[..]).is_ok()\n}","tryCatchPattern":"match decode_receipt_logs(&receipt_bytes) {\n    Ok(logs) => { /* proceed: find_deposit_log_indices, verify proof */ }\n    Err(e) if e.to_string().contains(\"invalid log\") => {\n        // Malformed proof data: reject the proof; never retry with the same bytes.\n        return Ok(ProofOutcome::Rejected);\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Only feed proof bytes obtained from verified Merkle proofs against the light client's receipts root.","Round-trip test every producer's receipt encoding against alloy_primitives::Log::decode.","Reject, don't retry: a decode failure means the bytes are bad, not that the system is busy."],"tags":["rlp","ethereum","receipt","decoding","bridge"],"backgroundTag":"rlp-decoding-failed","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}