linera-io/linera-protocol · error

block header payload extends past available data

Error message

block header payload extends past available data

What it means

After decoding the outer list header, decode_block_header checks that the remaining bytes cover the declared payload_length. alloy_rlp::Header only strips the prefix, not the payload, so if the buffer is truncated relative to the declared size this ensure! fires. It is a deliberate bounds check that prevents out-of-bounds slicing at `&data[..payload_length]` and DoS via oversized length claims.

Source

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

        let (root, proof) = super::build_receipt_proof(receipts, target_tx_index);
        (root, proof.into_iter().map(Into::into).collect())
    }
}

/// Decodes an RLP-encoded Ethereum block header, returning `(block_hash, receipts_root)`.
///
/// The block hash is `keccak256(header_rlp)`. The receipts root is field index 5
/// in the RLP list.
pub fn decode_block_header(header_rlp: &[u8]) -> Result<(B256, B256)> {
    let block_hash = keccak256(header_rlp);

    let mut data = header_rlp;
    let list_header =
        alloy_rlp::Header::decode(&mut data).map_err(|e| anyhow!("invalid RLP header: {e}"))?;
    ensure!(list_header.list, "block header must be an RLP list");

    // Limit reads to the header's declared payload.
    ensure!(
        data.len() >= list_header.payload_length,
        "block header payload extends past available data"
    );
    let mut data = &data[..list_header.payload_length];

    // Skip first 5 fields: parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot
    for i in 0..5 {
        skip_rlp_item(&mut data).map_err(|e| anyhow!("failed to skip header field {i}: {e}"))?;
    }

    // Field index 5: receiptsRoot (B256)
    let receipts_root = <B256 as alloy_rlp::Decodable>::decode(&mut data)
        .map_err(|e| anyhow!("failed to decode receipts_root: {e}"))?;

    Ok((block_hash, receipts_root))
}

/// Maximum number of MPT proof nodes accepted.

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Log the input length vs declared payload_length to confirm truncation, then fix the upstream producer/transport
  2. Fetch headers with full bodies from a trusted RPC and pass bytes through unmodified
  3. For cross-chain support of non-standard headers (e.g. OP-stack extra fields), verify against the chain's actual header spec before parsing
Defensive patterns

Strategy: try-catch

Try / catch

match decode_block_header(header_rlp) {
    Ok(v) => v,
    Err(e) if e.to_string().contains("extends past available data") => {
        // truncated or malformed input: re-fetch rather than retry
        anyhow::bail!("truncated block header ({} bytes); refetch from RPC", header_rlp.len())
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Passing a header that was cut short (partial read from RPC, truncated proof, split message framing); malformed input where the RLP length prefix declares more bytes than actually follow.

Common situations: Fixed-size buffer truncation upstream; an L2/non-standard chain emitting headers with extra fields after receiptsRoot such that an intermediate parser sliced too early; network transport delivering partial frames.

Related errors


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