linera-io/linera-protocol · error

invalid log RLP: {e}

Error message

invalid log RLP: {e}

What it means

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.

Source

Thrown at linera-bridge/src/proof/mod.rs:551

/// Skips one RLP item (string or list) by reading its header and advancing past the payload.
fn skip_rlp_item(data: &mut &[u8]) -> Result<()> {
    let header = alloy_rlp::Header::decode(data).map_err(|e| anyhow!("invalid RLP item: {e}"))?;
    ensure!(
        data.len() >= header.payload_length,
        "not enough data to skip RLP item"
    );
    *data = &data[header.payload_length..];
    Ok(())
}

/// Decodes a single log entry from RLP.
///
/// Enforces the declared payload boundary: after decoding address, topics, and data,
/// verifies that exactly `payload_length` bytes were consumed.
fn decode_log(data: &mut &[u8]) -> Result<ReceiptLog> {
    let log_header =
        alloy_rlp::Header::decode(data).map_err(|e| anyhow!("invalid log RLP: {e}"))?;
    ensure!(log_header.list, "log must be an RLP list");
    ensure!(
        data.len() >= log_header.payload_length,
        "log payload extends past available data"
    );

    // Limit reads to the declared payload boundary.
    let mut log_data_buf = &data[..log_header.payload_length];
    *data = &data[log_header.payload_length..];

    let address = <Address as alloy_rlp::Decodable>::decode(&mut log_data_buf)
        .map_err(|e| anyhow!("invalid log address: {e}"))?;

    // Decode topics list
    let topics_header = alloy_rlp::Header::decode(&mut log_data_buf)
        .map_err(|e| anyhow!("invalid topics list RLP: {e}"))?;
    ensure!(topics_header.list, "topics must be an RLP list");
    ensure!(

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. 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.
  2. Check the slicing/skipping logic that positions the cursor before the log loop (status byte, cumulativeGasUsed, logsBloom) for off-by-one errors.
  3. Re-fetch the receipt and Merkle proof from the EVM RPC and retry verification with fresh bytes.
  4. Confirm the proof was produced for the same chain/hardfork the light client tracks.

Example fix

// before: raw bytes from the proof are handed straight to the log decoder
let logs = decode_receipt_logs(&receipt_bytes)?; // fails: "invalid log RLP: ..."

// after: pre-validate the whole log with the reference decoder, reject the proof on mismatch
use alloy_rlp::Decodable;
if alloy_primitives::Log::decode(&mut &log_bytes[..]).is_err() {
    anyhow::bail!("rejecting receipt proof: log entry is not canonical RLP");
}
let logs = decode_receipt_logs(&receipt_bytes)?;
Defensive patterns

Strategy: validation

Validate before calling

// Cheap pre-check: the reference decoder must accept the whole log entry
// before the custom field-by-field proof parser runs.
use alloy_rlp::Decodable;
fn is_canonical_log(rlp: &[u8]) -> bool {
    alloy_primitives::Log::decode(&mut &rlp[..]).is_ok()
}

Type guard

fn is_canonical_log(rlp: &[u8]) -> bool {
    use alloy_rlp::Decodable;
    alloy_primitives::Log::decode(&mut &rlp[..]).is_ok()
}

Try / catch

match decode_receipt_logs(&receipt_bytes) {
    Ok(logs) => { /* proceed: find_deposit_log_indices, verify proof */ }
    Err(e) if e.to_string().contains("invalid log") => {
        // Malformed proof data: reject the proof; never retry with the same bytes.
        return Ok(ProofOutcome::Rejected);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: 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).

Common situations: 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).

Related errors


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/abedde0a9861bbe7. Report an issue: GitHub.