linera-io/linera-protocol · error

not enough data to skip RLP item

Error message

not enough data to skip RLP item

What it means

skip_rlp_item is a helper used by decode_block_header and decode_receipt_logs to step over RLP items (status, cumulative gas used, logs bloom) by reading an alloy_rlp Header and advancing past header.payload_length. The ensure at linera-bridge/src/proof/mod.rs:537 fails when the remaining buffer is shorter than the declared payload, i.e. the byte string claims more content than actually present. This is the classic signature of truncated or corrupted RLP — often a receipt pulled from a bad Merkle-proof node.

Source

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

    for (key, value) in &entries {
        builder.add_leaf(*key, value);
    }

    let root = builder.root();
    let proof_nodes = builder.take_proof_nodes();
    let proof = proof_nodes
        .matching_nodes_sorted(&target_key)
        .into_iter()
        .map(|(_, bytes)| bytes.to_vec())
        .collect();

    (root, proof)
}

/// 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"

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Log the failing field index (the caller wraps this as 'failed to skip receipt field {i}') to see how far decoding got before the shortfall.
  2. Re-fetch the receipt RLP and its proof from the node; verify the proof against the block's receipts_root before parsing.
  3. Check that the receipt bytes were not truncated in transit: total length should exactly cover the outer list header plus payload_length.
  4. Confirm the receipt type matches the chain's active EIPs (post-1559/4844 receipts carry typed prefixes; ensure the leading type byte was stripped before RLP decoding).

Example fix

// before
for i in 0..3 {
    skip_rlp_item(&mut data).map_err(|e| anyhow!("failed to skip receipt field {i}: {e}"))?;
}

// after: fail fast with a boundary check before parsing
ensure!(data.len() >= list_header.payload_length, "receipt truncated: have {}, declared {}", data.len(), list_header.payload_length);
for i in 0..3 {
    skip_rlp_item(&mut data).map_err(|e| anyhow!("failed to skip receipt field {i}: {e}"))?;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Structural pre-check before decode_receipt_logs: outer receipt must cover its payload.
fn receipt_rlp_bounds_ok(mut data: &[u8]) -> bool {
    if data.first() == Some(&0x80) || data.first().is_some_and(|b| *b < 0x80) { data = &data[1..]; }
    match alloy_rlp::Header::decode(&mut data) {
        Ok(h) => h.list && data.len() >= h.payload_length,
        Err(_) => false,
    }
}

Try / catch

match decode_receipt_logs(receipt_rlp) {
    Ok(logs) => logs,
    Err(e) if e.to_string().contains("not enough data to skip RLP item") => {
        // Truncated/corrupt node: drop the proof and re-fetch rather than retry the same bytes.
        tracing::warn!(error = %e, "receipt RLP truncated; discarding proof");
        Vec::new()
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: decode_receipt_logs walks a receipt whose RLP was cut short (truncated proof node bytes, wrong trie node delivered for the key, hex/nibble mismatch in the receipt trie key); decode_block_header reads a header whose payload length exceeds the input; a test fixture hand-assembles receipt RLP with a wrong length prefix.

Common situations: Merkle proof nodes from an RPC provider are truncated or from a different block; receipt bytes were copied with an off-by-one offset (extra or missing prefix byte, see the 0x80 single-byte handling above); network transport or storage cut the payload; receipt format from a chain with extra fields (e.g., EIP-4844 blob gas) that shifts field boundaries so a skip lands mid-item.

Related errors


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