Wire format, bit by bit

A byte-level tour of libtracer’s TLV — the protobuf-encoding-guide view of the format. Every example is a real frame, reproducible with tr::wire::encode (core/include/libtracer/frame.hpp).

This page is implementation-independent. It describes the bytes any conformant implementation puts on the wire, in any language; it lives under docs/modules/ for URL stability, not because anything here is specific to the C++ reference implementation. The normative text is the data-format reference and the TLV catalog; frame-codec is the C++ API that reads and writes these bytes.

The whole protocol is one shape, recursively: a Type-Length-Value. There are no field tags, no varints, no schema needed to walk the bytes — the header tells you everything, and a structured value is just more TLVs concatenated.

The header (4 bytes, or 6)

 ┌────────┬────────┬────────────────┐         ┌────────┬────────┬────────────────────────────────┐
 │  type  │  opt   │  length (u16)  │   or    │  type  │  opt   │        length (u32)            │
 │  u8    │  u8    │  little-endian │  LL=1   │  u8    │  u8    │        little-endian           │
 └────────┴────────┴────────────────┘         └────────┴────────┴────────────────────────────────┘
   byte 0   byte 1   bytes 2..3                  byte 0   byte 1   bytes 2..5
  • type — one byte. 0x01 VALUE, 0x02 NAME, 0x06 PATH, 0x07 POINT, 0x09 STATUS, 0x0B SETTINGS, 0x0C TIME, 0x0F FWD … (0x80–0xFF is yours).

  • opt — eight flag bits (below).

  • length — payload size, fixed-width little-endian: u16 normally, u32 when opt.LL=1. It counts payload bytes only: neither the header nor a trailer is included. Fixed width means a parser jumps header + length to the next TLV with no scanning — the basis of the iterative (non-recursive) walk.

The opt byte, bit by bit

   bit:   7      6      5      4      3      2      1      0
        ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
        │  R   │  PL  │  TS  │  CR  │  LL  │  CW  │  TF  │  R   │
        └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
          │      │      │      │      │      │      │      │
  reserved┘      │      │      │      │      │      │      └reserved  (both MUST be 0;
   (=0)          │      │      │      │      │      │                  non-zero ⇒ invalid)
                 │      │      │      │      │      │      └ TF  timestamp form: 0=abs u64 ns, 1=rel i32
                 │      │      │      │      │      └ CW  CRC width: 0=CRC-32C, 1=CRC-16-CCITT
                 │      │      │      │      └ LL  length width: 0=u16, 1=u32
                 │      │      │      └ CR  trailer carries a CRC
                 │      │      └ TS  trailer carries a timestamp
                 │      └ PL  payload is structured (children), not opaque bytes
                 └ (reserved)

So opt = 0x40 is 0b0100_0000PL=1 (structured). opt = 0x10CR=1 (CRC trailer). A frame pays bytes only for the options it sets; the default opt = 0x00 is a bare opaque value with a 4-byte header.

Worked frames

1 · empty STATUS = OK (4 bytes)

The smallest frame — a write acknowledgement.

 09 00 00 00
 │  │  └──┴── length = 0x0000 = 0  (no payload)
 │  └─────── opt    = 0x00         (no flags)
 └────────── type   = 0x09 STATUS

An empty STATUS is “OK”. No body, no enum — absence is the signal.

2 · a VALUE carrying one byte (5 bytes)

A boolean true.

 01 00 01 00 01
 │  │  └──┴─ │  length = 0x0001 = 1
 │  │        └─ payload[0] = 0x01   ← the value 'true'
 │  └────────── opt = 0x00
 └───────────── type = 0x01 VALUE

The payload bytes are opaque to the protocol — 0x01 means true only because the application’s schema says so. libtracer never interprets application data (just like JSON does not know a field is a temperature).

3 · a VALUE with a CRC trailer (13 bytes)

Same VALUE, payload AA BB CC DD EE, integrity-checked with CRC-32C.

 01 10 05 00 AA BB CC DD EE  B6 C9 12 23
 │  │  └──┴─ └──────────────┘ └──────────┘
 │  │   len=5    payload          trailer: CRC-32C(payload) = 0x2312C9B6,
 │  │                                       stored little-endian
 │  └─ opt = 0x10  → CR=1 (trailer has a CRC)
 └──── type = 0x01 VALUE

The CRC lives in the trailer, after the payload — not the header. That is what lets a recorder or forwarder attach integrity at egress and strip it at ingress without touching the payload bytes: at rest a value is header+payload; in transit it grows a trailer; the payload is byte-identical through both.

4 · a packed PATH /sensor/temp (16 bytes)

opt.PL=0 — the payload is not child TLVs. It is a self-delimiting run of segment records, each [u8 len][utf8] (RFC-0018).

 06 00 0C 00 │ 06 73 65 6E 73 6F 72 │ 04 74 65 6D 70
 └──┬───────┘ └──────────┬────────┘ └───────┬──────┘
    │           "sensor" (7 bytes)    "temp" (5 bytes)
    │           06=len  s e n s o r
    │
    type=0x06 PATH · opt=0x00 (PL=0) · length=0x000C=12  (= 7 + 5 record bytes)

Walking it is cheaper than the outer frame’s loop, not the same one: read one length byte, jump 1 + len, repeat — no option decode and no header to construct. A record carries no type byte and no option byte, which is the whole point: an address has exactly one spelling. And those 12 payload bytes are exactly the vertex-map key (path; path_t::key, core/include/libtracer/path.hpp): the address on the wire and the address in memory are the same bytes. len == 0 is the reserved escape record (00 <kind> <len> <bytes>) — a forwarder steps over one, a key rejects it.

5 · a FWD frame (the remote-operation envelope)

A remote write carried by the source-routed FWD (0x0F, reference/05 §reserved range): the op code, the explicit route to the target (dst), the accumulated way back (src), then the payload TLV — FWD{ op=WRITE, dst=/b/temp, src=(empty), VALUE 0x2A }, 29 bytes:

 0F 40 19 00                              ← FWD · opt=0x40 (PL=1) · length=0x0019=25
 │ 01 00 01 00 01                         ← VALUE op: 1 byte, WRITE=0x01
 │ 06 00 07 00                            ← PATH dst (PL=0), 7 body bytes
 │   01 62                                ←   record "b"    (the next-hop link)
 │   04 74 65 6D 70                       ←   record "temp" (the target on the peer)
 │ 06 00 00 00                            ← PATH src (PL=0), empty — grows per hop
 │ 01 00 01 00 2A                         ← VALUE payload: the byte 0x2A

Every child is one of the shapes above — the frame is examples 2 and 4, concatenated. A forwarding hop reads just the three leading headers by offset: it strips the record 01 62 ("b") from dst (shrinking it toward the target), prepends its own segment record for the inbound link to src (the return route), and sends the rest of the frame onward untouched — the payload bytes are never copied or re-encoded (rebuild_fwd_forward, core/include/libtracer/fwd_frame_view.hpp, emits two rebuilt headers and gathers every other region as an offset window into the source). When dst no longer starts with a link name, the frame has arrived: the terminus decodes it and applies the op.

Nothing wraps a FWD: routing is explicit and source-routed, and 0x0D ROUTER is a reserved, decodable codepoint with no implemented mechanism (reference/05 §0x0D).

The same bytes, three ways

There is no separate “decode into a struct” step. The wire bytes, the in-memory value, and the graph node are one buffer.

        flowchart LR
    B["bytes:<br/>06 00 0C 00 06 &quot;sensor&quot; …"]:::b
    B --> W["on the wire<br/>(a frame)"]
    B --> M["in memory<br/>(a borrowed view, no copy)"]
    B --> G["in the graph<br/>(the vertex's value / key)"]
    classDef b fill:#dbeafe,stroke:#1e40af;
    

Consequences of the layout

In the bytes

Consequence

4-byte header (type opt len)

tiny per-message overhead; fits MCU MTUs

fixed-width length

jump to the next TLV with no varint scan → an iterative, bounded, recursion-free parser

opt flag bits

timestamp/CRC/wide-length cost bytes only when set; the default frame is 4 bytes

trailer-positioned CRC/TS

attach/strip integrity & time without rewriting the payload (rest ⇄ transit)

PL=1 = concatenated children

structure with no list type; a structured value is parsed in place as sub-spans

payload = opaque bytes

the protocol is a transparent carrier; the application’s schema gives bytes meaning

the key bytes = the PATH payload

one address for wire and memory; dispatch is a byte compare

The bytes received are the bytes kept: a decoded value is a set of spans into the received buffer (views), so reading a field is a pointer load and handing a value to N subscribers is N refcount bumps rather than N copies.

API reference

Generated from core/include/libtracer/tlv.hpp by Doxygen.

struct opt_t

The 1-byte opt options bitfield of a TLV header.

Bits, MSB→LSB: R | PL | TS | CR | LL | CW | TF | R. Bits 7 and 0 are reserved-MUST-be-zero (a set reserved bit ⇒ frame::invalid). See docs/reference/01-data-format.md §header + opt.

Public Functions

inline constexpr std::uint8_t encode() const noexcept

Pack back into the raw opt byte (reserved bits always zero).

inline constexpr opt_t without_trailer() const noexcept

The same opt with the trailer bits (TS/CR/CW/TF) cleared — only the structural bits (PL/LL) survive.

An ADR-0041 §4 trailer-sliced whole-TLV copy (op_resolve.cpp) applies this so the copy, whose bytes exclude the trailer by construction, stays self-consistent — the typed replacement for the raw opt & 0x48 mask that once encoded these bits.

constexpr bool operator==(const opt_t&) const noexcept = default

Value equality over all option bits.

Public Members

bool pl = false

bit 6: payload-is-structured (children, not opaque bytes).

bool ts = false

bit 5: trailer carries a timestamp.

bool cr = false

bit 4: trailer carries a CRC.

bool ll = false

bit 3: length width (false = u16, true = u32).

bool cw = false

bit 2: CRC width (false = CRC-32C, true = CRC-16-CCITT).

bool tf = false

bit 1: timestamp form (false = abs u64, true = rel i32).

Public Static Functions

static inline constexpr bool reserved_set(std::uint8_t b) noexcept

True iff a reserved bit is set in raw byte b (⇒ the frame is invalid).

static inline constexpr opt_t decode(std::uint8_t b) noexcept

Unpack a raw opt byte b (reserved bits are checked separately).

Public Static Attributes

static constexpr std::uint8_t kReservedMask = 0b1000'0001

The reserved-MUST-be-zero bit mask (bits 7 and 0).

enum class tr::wire::type_t : std::uint8_t

The core TLV type-code registry (0x01-0x10, docs/reference/05 §per-type layout).

0x05 is retired (was LIST, ADR-0003). 0x0E SPEC is the in-band vertex-creation spec (ADR-0017); 0x0F FWD and 0x10 FIELD are the remote-operation frames (RFC-0004 / ADR-0035, the v1 fast-track range 0x0F-0x1F). All are structured (opt.PL=1) and handled generically by the codec. Codes 0x11-0x13 are transport-plane route-handle control frames (RFC-0004 §E.1, ADR-0035 slice 4): they ride a full-TLV link (ws/UDP) ALONGSIDE FWD to compact an established, delivery_compact-flagged flow into a per-link label. They are NOT part of the FWD frame and NOT cross-core conformance TLVs — a peer that ignores them simply keeps the full-route delivery path — but are self-describing (opt.PL=1) so the codec parses them generically. 0x14 PATH_REF is the bound-path address form (RFC-0024 §4) and 0x15 PATH_REF_REVERSE the reverse-direction list a mint-flagged request accumulates (§7.1 amendment 2): the two types whose body is NOT self-describing — a fixed-stride 8-byte record array (opt.PL=0), whose shape the grammar therefore checks by type (path_ref.hpp, gated by is_path_ref_type).

One code beyond the core range is named here: 0x80 BATCH, the single assignment inside the user range (0x80-0xFF, RFC-0025 §4.1.2 clause 6). It is an ordinary structured TLV to the codec — the enumerator exists so the one convention libtracer itself emits has a spelling, not because the codec treats it specially.

Values:

enumerator VALUE

Opaque scalar value.

enumerator NAME

UTF-8 name segment.

enumerator DESCRIPTION

Human-readable description.

enumerator SUBSCRIBER

Subscriber registration edge.

enumerator PATH

Path address; opaque body of packed [u8 len][utf8] segment records (RFC-0018), NOT NAME children.

enumerator POINT

A point in a path/graph.

enumerator ERROR

Error report.

enumerator STATUS

Status report.

enumerator ACL

Access-control list.

enumerator SETTINGS

QoS settings.

enumerator TIME

An APPLICATION-DOMAIN timestamp carried inside a structured payload — RESERVED, deliberately emitted and consumed by no core code (#1109). The wire-trailer TS (opt.ts) is transport-time; sample-acquisition / control-deadline time rides the payload as a TIME child instead (docs/reference/01-data-format.md §application-domain timestamps). What a TIME body means is the embedder’s schema, so core assigns the code and nothing else.

enumerator ROUTER

Router-wrapped frame.

enumerator SPEC

In-band vertex-creation spec (structured; ADR-0017).

enumerator FWD

Remote-operation forward frame (RFC-0004 §B / ADR-0035).

enumerator FIELD

Control-plane :field selector (RFC-0004 §C / ADR-0035).

enumerator ADVERTISE

Route-handle: VALUE label(u16) + PATH route — bind label→route, swapped per hop.

enumerator COMPACT

Route-handle: VALUE label(u16) + payload TLV — a label-compacted delivery.

enumerator HANDLE_NACK

Route-handle: VALUE label(u16) — stale/unknown label seen; prompts re-advertise.

enumerator PATH_REF

Bound path: a bare array of 8-byte node-scoped vertex refs (RFC-0024 §4).

enumerator PATH_REF_REVERSE

The REVERSE-direction bound path a mint-flagged request accumulates (RFC-0024 §7.1 amendment 2) — same body grammar as PATH_REF, different role.

A distinct code rather than a positional rule: every other element of this grammar self-describes by type, and “the only trailing child” would break the moment a future RFC adds a second trailing child to a mint-flagged request. It also un-forecloses a raw PATH_REF-typed payload on such a request. The code costs nothing: a reader already compares the child’s type byte, so a different constant is the same instruction (peek_trailing_mint).

enumerator BATCH

The ONE assigned user-range code: the BATCH record (RFC-0025 §4.1.2, Amendment 3 clause 6) — a structured (opt.PL=1) written value whose children are the sample frames of one flush (batch.hpp).

0x800xFF is the range the protocol does not opine on, and the assignment does not change that: a deployment already using 0x80 for its own record is not made non-conforming, no core-range code is minted, no opt bit is added, and the graph still never interprets the body (claim 5). What the assignment buys is ONE number — so the reference helpers, the §4.3 descriptor and the conformance vectors stop each picking their own. Until Amendment 3 this code appeared only as the worked example of docs/reference/05-protocol-tlvs.md §0x0C, which reads the same either way.

See: frame-codec · the normative data-format reference · the TLV catalog.