Skip to content
Docs

Parse the shred header

Turn an untrusted UDP datagram into validated metadata without unchecked offsets or guessed variants.

Before you start

  • Solana shred anatomy
  • An exact received datagram length

Parse in two layers

Use a small checked common-header parser to route work, then use the matching Agave typed parser before trusting any field. The first layer must never panic. It can read slot, index, type, version, and FEC-set index for metrics or partition selection. The second layer validates variant-specific sizes, flags, parent offsets, coding counts, proof capacity, and other invariants.

Do not deserialize the whole 1,228-byte receive array. Pass only the len bytes returned by recvmsg. A UDP short read is one truncated datagram, not a fragment to append to the next read.

Classify the variant

Legacy data and coding discriminator bytes are 0xA5 and 0x5A. Merkle forms encode proof entry count in the low nibble and type in the high nibble. Historical chained and unchained forms include data high nibbles 0x80, 0x90, and 0xB0, plus coding 0x40, 0x60, and 0x70. Current Agave releases can reject retired forms.

Classification is not validation. A random byte with the right high nibble is not a valid shred. After routing, construct the typed shred and call its sanitizer.

rust

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Kind { Data, Code }

#[derive(Debug)]
struct Common { kind: Kind, slot: u64, index: u32, version: u16, fec: u32 }

fn read<const N: usize>(b: &[u8], start: usize) -> Option<[u8; N]> {
    b.get(start..start + N)?.try_into().ok()
}

fn parse_common(b: &[u8]) -> Result<Common, &'static str> {
    let variant = *b.get(64).ok_or("common header shorter than 65 bytes")?;
    let kind = match variant {
        0xa5 => Kind::Data,
        0x5a => Kind::Code,
        v => match v & 0xf0 {
            0x80 | 0x90 | 0xb0 => Kind::Data,
            0x40 | 0x60 | 0x70 => Kind::Code,
            _ => return Err("unknown shred variant"),
        },
    };
    Ok(Common {
        kind,
        slot: u64::from_le_bytes(read(b, 65).ok_or("missing slot")?),
        index: u32::from_le_bytes(read(b, 73).ok_or("missing index")?),
        version: u16::from_le_bytes(read(b, 77).ok_or("missing version")?),
        fec: u32::from_le_bytes(read(b, 79).ok_or("missing fec_set_index")?),
    })
}

fn main() {
    if parse_common(&[]).is_ok() { std::process::exit(1); }
}

This code compiles with the standard library. It deliberately stops at the common header. It does not certify that the packet is usable.

Use Agave for the typed parse

With a pinned solana-ledger dependency, construct the protocol object:

rust

use solana_ledger::shred::Shred;

fn parse_typed(packet: &[u8]) -> Result<Shred, String> {
    let shred = Shred::new_from_serialized_shred(packet.to_vec())
        .map_err(|e| e.to_string())?;
    shred.sanitize().map_err(|e| e.to_string())?;
    Ok(shred)
}

fn describe(packet: &[u8]) -> Result<String, String> {
    let shred = parse_typed(packet)?;
    let mut out = String::from("slot=");
    out.push_str(&shred.slot().to_string());
    out.push_str(" index=");
    out.push_str(&shred.index().to_string());
    out.push_str(" fec=");
    out.push_str(&shred.fec_set_index().to_string());
    Ok(out)
}

Pin the crate version or Git revision that matches the cluster release. The ledger crate exposes some unstable implementation surface. Compile upgrades in a branch and replay fixtures before release.

Validate data fields

For a data shred, require a legal parent offset, flags, and declared size. last_in_slot must imply data complete. The typed data() method returns only the application data region after checking type and size. Do not slice from byte 88 to the datagram end because that includes padding and Merkle material.

Use data_complete() to find serialized data boundaries and last_in_slot() to find a proposed slot end. A slot can have multiple data-complete boundaries. Decode each consecutive completed range. Do not wait for the last-in-slot flag to emit all entries.

Validate coding fields

For a coding shred, parse num_data_shreds, num_coding_shreds, and position through the pinned implementation or a reviewed adapter. Require both counts to be nonzero and within protocol limits. Require position < num_coding_shreds. Derive local erasure index as num_data_shreds + position.

Multiple coding members of one (slot, fec_set_index) must agree on counts, first coding index, proof form, Merkle root, leader signature, and version. Reject the conflicting member and quarantine the set for evidence rather than letting the last packet overwrite configuration.

Treat version as a filter, not an upgrade signal

The shred version identifies cluster compatibility. Obtain the expected value from the network configuration used by your receiver. A mismatched version can indicate the wrong cluster, stale traffic, or malformed input. Do not select a parser implementation from an untrusted packet's version field.

Preserve evidence

Keep the original bytes until signature, Merkle, and duplicate-conflict checks finish. A typed structure may normalize or truncate input. Log only sampled metadata and a cryptographic digest, not every raw packet. Count every rejection by a bounded reason so a protocol upgrade appears as a clear change rather than a generic parse failure.

Keep routing output provisional

If the fast common parser and the typed parser disagree on slot, index, type, version, or FEC-set index, stop processing and count an internal parser disagreement. Do not allow the fast path's worker choice to change protocol identity. The owner can forward the typed result to the correct partition, but a disagreement normally means the manual reader no longer matches the pinned format.

Validate numeric ranges before using fields for allocation. A large slot is not itself invalid, but an index beyond the release's maximum data or coding shred count is. Check FEC-set index with the same release limit. Never allocate a vector up to an untrusted index; keep sparse bounded storage until sanitization and configuration checks complete.

Treat parser errors as data, not control flow for format discovery. A receiver must not switch to another layout because one packet failed. Cluster release and activation configuration select the parser. This prevents ambiguous byte sequences from being accepted under whichever interpretation happens to succeed.

Parameters

NameTypeDefaultNotes
packet&[u8]noneExactly the bytes returned by the UDP receive operation.
expected_shred_versionu16noneTrusted cluster configuration, never selected from the packet itself.
parser_revisionstringnonePinned Agave crate or source revision.

When it goes wrong

shred parse: Invalid shred variant

Cause. The variant is malformed, retired, or newer than the pinned parser.

Fix. Reject it, inspect sampled variant bytes, and compare against the network release.

shred sanitize: Invalid shred flags: N

Cause. A data header contains a flag combination outside the protocol rules.

Fix. Reject the packet before data extraction and retain a digest for correlation.

unsupported shred version: got N expected M

Cause. Traffic belongs to another cluster or the trusted configuration is stale.

Fix. Verify cluster identity and update through a tested release, not from packet input.

Questions

Why parse the common header before using Agave?
A checked common parser can route packets to workers and collect low-cost metrics without allocating a full protocol object. Its output remains untrusted. The Agave parser and sanitizer still decide whether variant-specific lengths, flags, counts, parent data, and proof layout are valid.
Should unknown variant bytes be ignored?
Reject and count them. Do not infer a new layout from the high nibble alone. An unknown value can be corrupt input or a network upgrade. Compare it with the release used by the cluster, update the pinned parser, and replay captured fixtures before accepting it.
Can data bytes be read from offset 88 to packet end?
No. The packet tail can contain padding, a chained Merkle root, proof entries, and a retransmitter signature. The data header's size and the parsed variant define the ledger-data slice. Call the typed data accessor after sanitization instead of hand-slicing the tail.