{"record":{"id":"9bcaddbb4daccdab","repo":"linera-io/linera-protocol","slug":"log-must-be-an-rlp-list","errorCode":null,"errorMessage":"log must be an RLP list","messagePattern":"log must be an RLP list","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"linera-bridge/src/proof/mod.rs","lineNumber":552,"sourceCode":"/// 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!(\n        log_data_buf.len() >= topics_header.payload_length,","sourceCodeStart":534,"sourceCodeEnd":570,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-bridge/src/proof/mod.rs#L534-L570","documentation":"decode_log decodes one Ethereum log entry, which per the receipts specification must be an RLP list of exactly three fields: address, topics list, data byte string. The ensure at linera-bridge/src/proof/mod.rs:552 fails when the outer item's header declares a string (list flag false) instead of a list. That means the bytes being parsed as a log are not a log at all — usually the wrong slice of a trie node or malformed RLP.","triggerScenarios":"decode_receipt_logs iterates the logs list and hands decode_log a slice that starts at a non-list item: a trie node delivered in place of the receipt, a hash-reference where inline data was expected, or a fixture that RLP-encodes a log as a flat string.","commonSituations":"Merkle proof returns an extension/branch node instead of the leaf containing the receipt; receipt bytes offset by one so decoding starts on a nested string; a node implementation or test helper that encodes logs inconsistently with yellow-paper rules.","solutions":["Hex-dump the first bytes handed to decode_log; a leading byte below 0xC0 is a string header and proves the slice is misaligned or the node is not a log.","Verify the receipt proof (build_receipt_proof / trie verification) against the block header's logs/receipts root before attempting to decode logs.","Check that iteration over logs_data starts exactly at the first log item after the logs-list header, not after the receipt's skipped fields.","Re-fetch the block/receipt from a trusted provider and compare bytes to detect provider-side corruption."],"exampleFix":"// before\nlet log_header = alloy_rlp::Header::decode(data).map_err(|e| anyhow!(\"invalid log RLP: {e}\"))?;\nensure!(log_header.list, \"log must be an RLP list\");\n\n// after: distinguish string vs list for clearer diagnostics\nlet log_header = alloy_rlp::Header::decode(data).map_err(|e| anyhow!(\"invalid log RLP: {e}\"))?;\nensure!(log_header.list, \"log must be an RLP list; got a byte string of payload {} at offset — node is not a log\", log_header.payload_length);","handlingStrategy":"try-catch","validationCode":"fn next_item_is_list(data: &[u8]) -> bool {\n    alloy_rlp::Header::decode(&mut &data[..])\n        .map(|h| h.list)\n        .unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"match decode_receipt_logs(receipt_rlp) {\n    Ok(logs) => logs,\n    Err(e) if e.to_string().contains(\"log must be an RLP list\") => {\n        tracing::warn!(\"proof node is not a log entry; re-fetching receipt\");\n        Vec::new()\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Validate trie proofs before decoding contents; a wrong node usually surfaces as schema errors like this.","Keep cursor arithmetic (headers, payload slices) centralized instead of manual index math.","Prefer library RLP decoding over hand-parsed byte offsets for nested structures."],"tags":["rlp","ethereum","decoding","merkle-proof","rust","linera-bridge"],"backgroundTag":"rlp-decoding-error","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}