libtracer protocol — version 1 (DRAFT)¶
Status: DRAFT. The wire format is not yet stable. Pin to a specific commit if you depend on this.
This document is normative. It uses the keywords MUST, MUST NOT, SHOULD, SHOULD NOT, MAY as defined in RFC 2119.
1. Scope¶
This specification defines the libtracer protocol: the wire format (frame
header, options bits, fixed-width lengths, trailer timestamp/CRC), the core
TLV type registry and each type’s payload layout, addressing (canonical
PATH form and constraints), the error model (the tr::<concept>::<error>
registry), the data API semantics an implementation must expose
(read / write / await plus the :-field control surface — there are no
connect/disconnect/subscribe wire operations; subscription is a
SUBSCRIBER write into :subscribers[]), and the conformance procedure.
It deliberately does not specify: the module/host ABI (implementation-
defined, ADR-0013); a transport’s native
framing below the TLV layer (each transport documents its own binding);
application payload semantics (a VALUE is opaque to the protocol); or
discovery (a separate, layered concern).
2. Terminology¶
The canonical project vocabulary — vertex, edge, path, view, segment, rope, TLV, address-shift slicing, transport vertex, and the rest — is defined in the root glossary, CONTEXT.md, which this specification incorporates by reference for terminology. Where this document and the glossary disagree, this document wins.
For the purposes of section 3.1, the following additional terms are defined:
Path handle. An opaque, stable identifier for a vertex address that points (directly or indirectly) at a pre-encoded PATH TLV in the implementation’s read-only data segment. A path handle MUST resolve to the same byte sequence for the lifetime of the node.
Pre-encoded PATH TLV. A PATH TLV whose wire bytes are emitted at build time (e.g., as
static constdata in.rodata) or computed exactly once at node initialization, and never re-encoded thereafter.Hot path. Any code path executed per-message during steady-state operation (typically inside a publisher’s sample loop, an ISR, or a router dispatch). The hot path is contrasted with the init path, which executes once at node startup.
3. Wire format¶
The wire format is specified normatively by incorporation (ADR-0007, RFC-0001 §A.2):
docs/reference/01-data-format.md — the frame layout: 4/6-byte header, the
optbits (R|PL|TS|CR|LL|CW|TF|R, reserved bits MUST be zero), fixed-width u16/u32 length peropt.LL, the optional trailer (timestamp peropt.TS/opt.TF, CRC-32C or CRC-16-CCITT peropt.CR/opt.CW), little-endian multi-byte fields, and the iterative- parsing requirement (nesting depth receiver-resource-bounded, RFC-0006).docs/reference/05-protocol-tlvs.md — the type-code registry (
0x00sentinel;0x01–0x1Fcore;0x05reserved with no assigned meaning;0x0Dreserved codepoint with no mechanism;0x06PATH, the canonical address form, whose body is not a child sequence —opt.PLMUST be 0 and the body is zero or more segment records, each[u8 len][len bytes of UTF-8], in order, walked asp += 1 + body[p], withlenin1..64;len == 0is the escape record00 <u8 kind> <u8 len> <len bytes>, which is admissible in a frame path (a hop that does not implementkindMUST step over it by its declared length rather than reject the frame it is relaying) and MUST be rejected wherever a path is used as a canonical key,kind = 0x16being the path element of RFC-0029 —len = 8, au32LE vertex index then au32LE generation, any other length malformed and refusing the address, never the frame (RFC-0018 §5 and §5.4 amendment 1; this reverses RFC-0004 §134’s “children MUST be NAME” invariant,NAME(0x02) surviving for SETTINGS keys and:schemalabels);0x14is retired as an address form and a host MUST refuse a frame that presents it as adst(RFC-0029 §5.3); and0x15PATH_REF_REVERSE, the reverse-direction chain a subscribe request accumulates, whose body is aPATHbody of path elements (RFC-0029 §7.1, the type code being RFC-0024 §7.1 amendment 2’s) together with the routing semantics of the one address form (§0x06§path element PAIR, RFC-0029 §§4–10): a path element is a NAME or a PAIR, node-scoped, read head-first by exactly one node, one element per node’s whole local part, and NAMEs and PAIRs mix freely in one frame path; a PAIR is validated by bounds check, generation compare and registration, and a host that cannot validate one MUST NOT forward, MUST NOT apply the operation, MUST NOT attempt any repair, and MUST answertr::path::not_foundto a non-emptysrcon every arm, the origin falling back to the canonical string it still holds; a PAIR dereferencing to a point-to-point connection vertex with a non-empty residual egresses over that link withdstshrunk by exactly that element andsrcgrown canonically, never by a PAIR; a PAIR MUST NOT egress through a shared (bus) mount; a PAIR as the last element is the terminus, and a PAIR followed by more elements on any other vertex istr::path::invalid; the authorization check at every hop is a function of the dereferenced vertex, the caller and the right, never of the spelling, so a generation match authorizes nothing and a PAIR is an address, never a capability; a host relaying or issuing aREPLYprepends to itssrcits own part of the forward route — as a PAIR when it can issue one, as the canonical NAME run otherwise, never nothing — and learning adds no frame, flag or setup exchange, theFWDopbyte keeping itsop & 0x3Fmasking rule with bits 7–6 reserved and MUST be zero; a hop relaying a subscribe request prepends its reverse-direction element (PAIR or NAME run) to the request’sPATH_REF_REVERSEchild, which is identified by its type code and never by its position, and a subscription edge that holds no chain learns one on its first delivery by giving it a non-emptysrcthat names its node’s single learn endpoint with the edge in the tail; the hop consuming the final element of a chaindstre-heads its egressdstas a canonical emptyPATH, so a client that never speaks the PAIR is never answered in it (RFC-0024 §7.1 erratum 3); a generation MUST saturate, never wrap, and advances on retirement, on a point-to-point child’s tenancy change (a same-named re-add is a new identity) and on its link going down with the tenancy kept, a link whose transport cannot report a session boundary carrying a per-boot epoch once per session or advertise, never per frame, whose change counts as one; there is no withdraw frame, no unbind, no lease and no TTL; and a forwarding hop holds no hard state and no per-request state, RFC-0004 §E.1’sCOMPACTroute handle being the single named exception — and every core type’s byte-precise payload layout, including theERRORmodel (RFC-0002, accepted) and the remote-operationFWD/FIELDframes (RFC-0004). This document is incorporated in full, not only for its layouts: it is also where §1’s promised data-API semantics land — theread/write/awaitand subscribe/delivery clauses and the:-field control surface — together with the ACL model (access-mask bits, inheritance, evaluation order, and the denial codes) that §4’saclvector category tests. A PAIR-bearingPATHis never admissible as a canonical key (§30x06); issuing and honouring PAIRs are optional at both ends, and a host that implements neither steps over the escape record by its declared length.docs/reference/03-addressing.md §path syntax — canonical PATH constraints (segment limit 255 (RFC-0023), NAME limit 64 bytes, total ≤ 1024 bytes, reserved characters, UTF-8).
Those documents’ MUST/SHOULD/MAY clauses are normative clauses of this
specification. Behavioral notes marked informative in them are not. Where
this section’s incorporated documents and any other document disagree, the
incorporated text wins. There is no per-frame protocol-version field:
version negotiation is a discovery-layer concern
(ADR-0002); an incompatible
peer is reported as tr::version::mismatch.
3.1 Static path-handle conformance¶
This subsection is normative. It exists to make libtracer usable on MCU-class devices (Cortex-M0/M3/M4, ESP32, RISC-V microcontrollers) where the hot path MUST NOT allocate, format strings, or walk a parser tree to address a vertex.
3.1.1 Path-handle invariant¶
A conforming implementation MUST provide a way for an application to obtain a path handle for any path it intends to address more than once. The handle:
MUST resolve, deterministically and without allocation, to a byte sequence equal to the canonical PATH TLV (
type=0x06,opt.PL=0, a body of packed[u8 len][utf8]segment records per docs/reference/05-protocol-tlvs.md §0x06PATH, RFC-0018 §5) for the named vertex. A path handle is a canonical key, so its bytes MUST NOT contain an escape record (§30x06).MUST remain valid for the lifetime of the node, or until the application explicitly releases it.
MUST be cheap to copy (the implementation defines whether this is a pointer, a small integer index, or a struct-by-value).
MUST NOT require string parsing,
snprintf, ormallocon the hot path.
The byte-equivalence requirement (1) is the load-bearing one: a write through a path handle MUST be indistinguishable on the wire from a write through the equivalent string-form path, after both are canonicalized. (Informative: under the packed body a segment record carries no per-segment option byte, so an address has exactly one spelling and this equivalence is structural rather than conventional — RFC-0018 §5.1.)
3.1.2 Static encoding at build time¶
A conforming implementation MUST permit pre-encoded PATH TLVs to be placed in read-only memory (.rodata, flash, or equivalent) and used directly as the source of bytes for a path handle. No copy from .rodata to RAM is required.
The PATH TLV byte layout is fully specified by docs/reference/01-data-format.md and docs/reference/05-protocol-tlvs.md §0x06. A build-time encoder (macro, code generator, or hand-written byte literal) producing those bytes MUST satisfy:
typebyte =0x06.opt.PL= 0 — the body is a packed record run, not a child sequence (RFC-0018 §5).lengthfield width selected peropt.LL; total payload size MUST match the sum of the segment records’ sizes (1 + leneach).Each segment is one record
[u8 len][len bytes of UTF-8],lenin1..64, payload the segment’s UTF-8 bytes (no NUL terminator). A pre-encoded PATH TLV is a canonical key, solen == 0(the escape record, §30x06) MUST NOT appear in it.All multi-byte fields in little-endian per docs/reference/01-data-format.md §frame layout.
Path constraints from docs/reference/03-addressing.md §path syntax (segment limit 255, name limit 64 bytes, total ≤ 1024 bytes measured as the PATH TLV’s
lengthfield) MUST be checked at encode time. A pre-encoded PATH TLV that violates these limits is non-conforming. (Informative: under the packed body a one-byte segment costs 2 bytes, so the 1024-byte budget admits up to 512 records and the 255-segment count binds first for short segments; the byte limit still binds first once the mean segment exceeds 3 bytes. Under the retired NAME-TLV body each segment cost4 + lenand the byte limit bound first at 204 segments, so the count clause could never fire — RFC-0023 §4, crossover per RFC-0018 §5.)
3.1.3 Init-time registration¶
When path handles cannot be statically derived (e.g., interpolated peer-id mounts under /peer/{peer_id}), a conforming implementation MUST provide an init-time registration call that:
Accepts a string-form path.
Validates per the rules of docs/reference/03-addressing.md.
Allocates a single, reusable PATH TLV (in a long-lived segment).
Returns a path handle equivalent to the build-time form.
The registration call MAY perform allocation. The path handle it returns MUST satisfy 3.1.1 thereafter — including the no-allocation requirement on subsequent uses.
3.1.4 Hot-path API requirement¶
The implementation MUST expose a write entry point that takes a path handle (not a string) and a value TLV. Calling this entry point MUST NOT:
Allocate memory (other than what the dispatch / fanout itself requires for subscriber views).
Format a string from numeric components.
Parse the path handle’s bytes (the handle is already a validated, canonicalized PATH TLV).
Invoke per-segment hashing or comparison if the implementation uses a path-handle-keyed dispatch table.
The corresponding read and await entry points MUST satisfy the same constraints.
A string-form convenience entry point (e.g., tracer_write_str("/sensor/temp", tlv)) MAY be provided for ergonomics on hosts where the cost of string parsing is acceptable. It is NOT required, and a minimum-feature implementation (P0) MAY omit it entirely.
3.1.5 Inline-segments vs reference-segments (informative)¶
Two implementation strategies satisfy 3.1.1–3.1.4. Both are conformant.
Inline-segments: the pre-encoded PATH TLV holds its segment records inline as a concatenated record run in
.rodata. The handle is{ const uint8_t *bytes; size_t len; }. Suited to bare-metal / Cortex-M targets.Reference-segments: the pre-encoded PATH TLV is assembled from offsets into an interned-segment table. The handle is a small integer index. Suited to hosts where the same segment recurs across many paths and table reuse pays off.
Senders MAY choose either strategy. The wire bytes a path handle resolves to MUST be identical regardless of strategy.
4. Conformance¶
An implementation is libtracer v1 compatible if and only if it:
Honors every MUST clause in this document.
Passes every test vector under
tests/conformance/vectors/v1/.
Vectors are organized vectors/v1/<category>/<case>/ (framing, crc,
tlv-types, path, fwd, field, errors, acl, …), each case a
self-describing input.bin with a human-readable expected.json; a conforming
codec MUST round-trip each input.bin byte-exactly (encode(decode(x)) == x).
New categories are added alongside spec additions; adding a vector is not a
spec change, changing an existing vector’s bytes is.
There is no other compatibility test.
5. Versioning¶
Spec versions are integers. v1 is immutable once finalized — corrections that change conformance behavior require v2.
Changelog¶
unreleased — initial draft.
unreleased — added §3.1 static path-handle conformance (MCU-friendly addressing without runtime string formatting).
unreleased — §1 scope, §2 terminology (glossary by reference), §3 wire format normative-by-incorporation of reference/01 + 05 + 03 §path-syntax, §4 vector layout (RFC-0001 §A.2/§B/§E; RFC-0002 error model).
unreleased — §3 and §3.1.2: the canonical PATH segment cap is repriced from 32 to 255, and the encode-time limits sentence gains the informative note that the byte budget binds first (RFC-0023).
unreleased — §3, §3.1.1, §3.1.2 and §3.1.5: a
PATH(0x06) body stops being a sequence of childNAMETLVs and becomesopt.PL=0plus a self-delimiting run of[u8 len][utf8]segment records, withlen == 0reserved as the escape record — admissible in a frame path, rejected in canonical/key context (RFC-0018 §5 and §5.4 amendment 1, accepted 2026-08-06). This reverses RFC-0004 §134’s “children MUST be NAME” invariant; the §3.1.2 segment-cap note is re-derived for the packed body, where the 255-segment count binds first for short segments.unreleased — §3: a remote
AWAIT’s deadline belongs to the requester (RFC-0004 Amendment 3, accepted 2026-10-03, ships as v0.18.0).await_timeoutbecomes a requester-side hint the terminus MAY ignore; a terminus MUST NOT hold the receive context of the arriving link while anAWAITis pending, and is no longer required to answerERROR(TIMEOUT). No wire byte changes; thefwd-await-timeoutvector keeps its bytes.unreleased — §3: one path primitive (RFC-0029, accepted 2026-09-30, ships as v0.18.0). The escape record’s
kind = 0x16becomes the 8-byte(u32 index, u32 generation)PAIR element, replacing RFC-0027’s 4-byte path label;0x14PATH_REF is retired as an address form;0x15PATH_REF_REVERSE keeps its code with aPATHbody;FWDopbit 7 is retired (bits 7–6 reserved, MUST be zero); a reply’ssrccarries the forward route (NAME or PAIR per hop, never nothing, amending RFC-0004 §B);NOT_FOUNDanswers every refusal arm; authorization is spelling-independent; a generation bumps on tenancy change and link loss, with a per-boot epoch for session-less transports. The reference implementation and the affected conformance vectors follow slice by slice (RFC-0029 §13.2 S1–S8); until then they carry the superseded RFC-0024/RFC-0027 form.unreleased — §3: the path label is incorporated (RFC-0027 §§4–8, accepted 2026-08-15, amendments 4–7 ratified) — the escape record’s
kind = 0x16element, its node scope, its passive mint-on-reply distribution, thetr::path::not_found-and-fall-back-to-strings rule, saturate-and-retire generations, and the no-withdraw/no-aging rule. This adds no type code and no frame:0x16–0x1Fstay unassigned in the type registry, every existing conformance vector is byte-unchanged, and aPATHwith no label element is byte-identical to before. Minting is optional at both ends and off unless an implementation is given a label table.