Skip to content
Docs

Run a minimal viable receiver

Confirm end-to-end delivery by printing common shred headers from live datagrams.

Before you start

  • Rust installed
  • A verified public UDP destination
  • Firewall access for 203.23.178.228

Compile and run the receiver

Save the program below as receiver.rs. It uses only the Rust standard library, binds the command-line address, rejects sources other than 203.23.178.228, checks common-header length, and classifies known legacy and Merkle variants. Each output row reports source and datagram length, followed by FEC-set index, version, slot, and shred index.

rust

use std::{env, io::{self, Write}, net::{Ipv4Addr, SocketAddr, UdpSocket}}; const SOURCE: Ipv4Addr = Ipv4Addr::new(64, 130, 40, 90);const MAX_DATAGRAM: usize = 1228;const COMMON_HEADER: usize = 83; fn u16le(b: &[u8]) -> u16 {    u16::from_le_bytes(b.try_into().expect("two bytes"))} fn u32le(b: &[u8]) -> u32 {    u32::from_le_bytes(b.try_into().expect("four bytes"))} fn u64le(b: &[u8]) -> u64 {    u64::from_le_bytes(b.try_into().expect("eight bytes"))} fn shred_kind(variant: u8) -> Option<&'static str> {    match variant {        0xa5 => Some("legacy-data"),        0x5a => Some("legacy-code"),        v => match v & 0xf0 {            0x80 | 0x90 | 0xb0 => Some("merkle-data"),            0x40 | 0x60 | 0x70 => Some("merkle-code"),            _ => None,        },    }} fn report(mut line: String, error: bool) -> io::Result<()> {    line.push('');    if error { io::stderr().lock().write_all(line.as_bytes()) }    else { io::stdout().lock().write_all(line.as_bytes()) }} fn main() -> io::Result<()> {    let bind = env::args().nth(1).unwrap_or_else(|| "0.0.0.0:8001".into());    let socket = UdpSocket::bind(&bind)?;    report("listening on ".to_owned() + &bind + ", accepting source " + &SOURCE.to_string(), true)?;    let mut packet = [0u8; MAX_DATAGRAM];    loop {        let (len, peer) = socket.recv_from(&mut packet)?;        let allowed = match peer {            SocketAddr::V4(v4) => *v4.ip() == SOURCE,            SocketAddr::V6(_) => false,        };        if allowed == false { continue; }        if len < COMMON_HEADER {            report("short datagram: len=".to_owned() + &len.to_string(), true)?;            continue;        }        let variant = packet[64];        let Some(kind) = shred_kind(variant) else {            report("unknown variant byte=".to_owned() + &variant.to_string(), true)?;            continue;        };        let slot = u64le(&packet[65..73]);        let index = u32le(&packet[73..77]);        let version = u16le(&packet[77..79]);        let fec = u32le(&packet[79..83]);        let line = "slot=".to_owned() + &slot.to_string()            + " type=" + kind + " index=" + &index.to_string()            + " fec=" + &fec.to_string() + " version=" + &version.to_string()            + " len=" + &len.to_string() + " source=" + &peer.to_string();        report(line, false)?;    }}

The program is under 100 lines. Compile and start it with:

bash

rustc -O receiver.rs./receiver 0.0.0.0:8001

Replace port 8001 with the verified destination port. If the receiver sits behind static NAT, bind the private local address or 0.0.0.0, not the public translated address. At every firewall, admit 203.23.178.228/32 as the source for inbound UDP.

Understand what the program proves

Printed rows prove that service UDP reached the process and its first 83 bytes fit a recognized common-header layout. That header starts with 64 signature bytes. Variant and slot follow. Next comes index, with shred version before the FEC-set index. All displayed integers are little-endian in the documented layout.

The program does not validate the leader signature, Merkle proof, data or coding header, declared data size, parent relationship, or FEC configuration. It does not detect kernel receive-buffer overflow because the standard library does not expose SO_RXQ_OVFL ancillary data. It also performs synchronous text output inside the receive loop, which can cause loss when stdout blocks.

Use it for commissioning, not production. Stop it after confirming live output and move to a buffered receiver with SO_RCVBUF, overflow reporting, batching, bounded queues, metrics, and the Agave parser.

Check the output

Slots should generally move forward, but UDP can reorder packets and forks can expose competing proposals. Indexes advance independently for data and coding shreds. Many shreds share one fec value because it identifies their erasure set. Repeated output lines do not represent distinct shards.

Datagram lengths must be no greater than 1,228 bytes. A shorter packet is not automatically invalid because format variants determine their allowed size, so a production receiver should ask the matching Agave parser to sanitize it.

If output shows only unknown variant, capture its variant values, then check those against the network's Agave release. Do not add a guessed mask. Pin and test the parser from the matching release.

Diagnose no output

Run:

bash

sudo tcpdump -ni any udp port 8001 and src host 203.23.178.228

If packets appear there but the program prints nothing, confirm the bind address, port, process, and IP source. If no packets appear, inspect the cloud security group, host firewall, NAT rule, and verification status. Confirm socket existence with:

bash

ss -lunp 'sport = :8001'

Text printing becomes the bottleneck quickly. Redirecting output to a slow terminal or log collector can fill the socket queue. For a short test, sample one row per several hundred packets if needed. A production implementation should increment counters on the receive thread and send parsed metadata through a bounded channel.

Move to the protocol implementation

The next receiver must construct a typed shred with the matching Agave ledger crate and sanitize it. Resolve its scheduled slot leader, then verify the format's signed message with that leader's key. Group on (slot, fec_set_index), recover missing data where possible, order data indices, deshred at completion boundaries, and deserialize entries with that Agave release's codec.

Keep one short output sample with the deployment record. It should include both data and coding rows, several FEC-set values, the observed version, and datagram lengths. Do not retain a continuous terminal log. The sample proves commissioning state, while counters and bounded captures provide production evidence without blocking receipt.

Parameters

NameTypeDefaultNotes
bindSocketAddrV40.0.0.0:8001First command-line argument and verified destination port.
sourceIpv4Addr203.23.178.228Only source IP admitted by the program.
max_datagramusize1228Maximum documented UDP payload accepted without truncation.

When it goes wrong

bind: Address already in use

Cause. Another process already owns the selected UDP port.

Fix. Find it with ss -lunp, then stop it or use the verified port assigned to this receiver.

short datagram: len=N

Cause. A source-address-matching datagram does not contain the 83-byte common header.

Fix. Capture it, verify the sender path, and reject it before reading header fields.

unknown variant: 0xNN

Cause. The datagram uses an unsupported format or is not a valid shred.

Fix. Compare with the network's pinned Agave release and use its typed parser rather than adding a guessed value.

Questions

Does this receiver verify that shreds are genuine?
No. It filters one source IP and prints recognized common-header fields, but some paths permit address spoofing and malformed headers remain possible. Before trusting payload bytes, production must resolve the slot leader, validate the Merkle proof, and verify Ed25519 over the format-defined message.
Why does the example parse fixed common-header offsets?
This dependency-free program only commissions the documented layout. Production needs the parser belonging to the Agave release active on its network, because variants and serialization behavior change. The example rejects unknown variants instead of extending the format from an assumption.
Why can printing every packet cause drops?
Terminal and logging output can block long enough to fill the socket queue. The example keeps printing because visible output is its commissioning purpose. Production code must remove formatting from the receive loop and publish counters or sampled records asynchronously.