open-standards/joule-code/joule-code-rs/crates/joule-moq
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-20 08:30:25 -04:00
..
examples joule-moq: the session, mock, receipts and QUIC transport move onto draft-22 2026-10-16 10:07:07 -04:00
src Merge remote-tracking branch 'origin/main' into joule-moq/e2ee-web 2026-10-20 08:30:25 -04:00
tests joule-moq: the draft's AAD head checked in its encryption vectors 2026-10-19 20:26:47 -04:00
Cargo.toml joule-moq: secure_objects (WIP): seal and open per draft-ietf-moq-secure-objects-01 on -22 objects 2026-10-19 20:26:20 -04:00
README.md Text: CHANGELOGs, the relay's Receipts and token sections, joule-moq's and joule-moq-web's READMEs; registry entries for WJR1, WRP1, the grant Setup Options and HTTP fields 2026-10-18 15:46:41 -04:00

joule-moq

MoqT (Media over QUIC Transport) for CommunicationOS broadcast media, with a JWP receipt extension: every object carries verifiable provenance and its energy figure.

The draft it implements

draft-ietf-moq-transport-22 (1 October 2026), and no other. The draft is chosen before the first MOQT byte, by the ALPN moqt-22 on native QUIC or the WT-Available-Protocols value moqt-22 on WebTransport (§6.2); SETUP carries no version. MOQT_DRAFT_VERSION is the conventional code 0xff000016.

Drafts do not interoperate with each other. Deployed relays speak earlier drafts, which differ from -22 in their varint, their control streams and their messages, so this crate does not talk to them. The codec of -22 is a module of its own (src/draft22/), selected through draft::Draft: a later draft, or an earlier one added for interoperation, is a sibling module and a Draft variant, and the draft-neutral parts (tracks, objects, receipts, the session's state) stay as they are.

What is implemented

The codec (draft22), every decoder bounded (it reads no byte that is not there and allocates nothing from a length before the bytes behind it exist) and every refusal a typed WireError carrying the draft's consequence: close the session with a code, reject the request, or a malformed track.

-22 module
§8.1 variable-length integer: 1 to 9 bytes, leading ones, longer forms accepted draft22::varint
§8.2 Location, §8.7 Track Namespace (at most 32 fields, none empty, 4,096 bytes for a Full Track Name), §8.8 rendering draft22::names
§8.3 Key-Value-Pairs (delta types, values up to 2^16-1 bytes), §8.4 and §10 Properties with Immutable Properties, Prior Group and Object ID Gaps and Mandatory Track Properties, §9.1 Setup Options draft22::kvp
§8.9 Authorization Token structure draft22::token
§9 every control message of Table 5, framed by a 16-bit Length draft22::control
§9.20 every Message Parameter, the list each message allows, the rules on repetition; §8.6 Range Filters; §9.20.9 Location Filters; FILL_PARAMETERS draft22::params
§6.3 and §6.4.2 control and request streams, Request IDs by end, §9.1.7 MAX_REQUEST_UPDATES credit, the order of messages on each request stream draft22::request
§11 subgroup streams, object datagrams, fetch streams with End of Range, padding; a whole-stream decoder and an incremental one draft22::data
§12 and §16.11 session, request, PUBLISH_DONE and stream reset codes; §13 grease draft22::codes

On it:

  • session: the draft by ALPN, SETUP options, Request IDs, the receipt extension declared by a Setup Option.
  • transport_quic (feature quic): native QUIC on quinn. A pair of unidirectional control streams, each opened by SETUP; a bidirectional stream per request; subgroup, fetch and padding streams; object datagrams.
  • mock: an in-memory fabric and relay whose objects, receipts and requests cross the -22 codec.
  • carriage: how this crate's objects and receipts ride -22 objects (below).

Where -22 leaves an encoding open

The codec takes one reading of each, and the vector checker takes the same:

  • FILL_TIMEOUT (0x0A) and RENDEZVOUS_TIMEOUT (0x04) state no encoding; both are durations in milliseconds and are read as varints, as the draft encodes its other durations.
  • FILL_PARAMETERS is length-prefixed and holds "a sequence of Parameters": read up to its length, with no count before them, type deltas starting from 0.
  • End of Range (§11.4.1.2) says "the Group ID and Object ID fields are present"; the only such fields of a fetch object are the Group ID Delta and Object ID Delta, which bits 0x08 and 0x04 of 0x8C, 0x10C and 0x20C mark. They are read as an object's are: absolute on the first entry, and after it a Group ID that moves one or more groups in the request's Group Order, with an absolute Object ID.
  • An unknown message, stream or datagram type closes the session; -22 names no code, and this codec gives PROTOCOL_VIOLATION.

Carriage of this crate's objects and receipts

Object Properties in the range -22 sets aside for an application's own use (§8.4), inside the object's Immutable Properties, since a relay must not change them:

type name value
0x3A40 MRL_LAYER varint: the object's layer, when the header states layers
0x3A41 WAI_RECEIPT bytes: WAI extensions/moq-streaming-format.md §5's property, laid out below
0x3A42 MRL_TOTAL_LAYERS varint, when non-zero
0x3A43 JWP_OBJECT_RECEIPT bytes: the object receipt standing alone, in the binary form of WAI extensions/jwp-receipts.md §2.7.2
0x3A44 WAI_ENVELOPE varint 1, when the payload is a WAI envelope

A layer cap on a SUBSCRIBE is an OBJECT_PROPERTY_FILTER on MRL_LAYER (messages::mrl_cap_filter), which needs the peer's MAX_FILTER_RANGES above 0. An object's delivery timeout rides as its OBJECT_DELIVERY_TIMEOUT property (§10.2).

The WAI_RECEIPT value: content_hash (32 bytes), joules_micro (a -22 varint), acquisition (a varint length, then UTF-8), acquisition_evidence (a varint length, then bytes), group_receipt (32 bytes), and nothing after.

A group's receipts ride its receipts track (moq-streaming-format §4.1): group g carries one object, the group receipt and the object receipts of group g of the media track in the binary form of WAI jwp-receipts §2.7.3: WJR1, the group receipt's canonical bytes (its signing payload in the form signed, then its signature, and for a legacy group its parent flag and parent), a u32 count, and each object receipt's body. ReceiptBundle::read gives the group's link hash computed over the bytes received; read_receipts, GroupReceipt::from_binary and ObjectReceipt::from_binary refuse what does not read with the §3 step jwp-receipts §2.7.4 names. tests/wai_jwp_corpus.rs reads and writes every binary vector of WAI's corpus.

Room grants (WAI extensions/live-rooms.md §10.5, grant_proof): a grant is public, so it is presented with a WRP1 proof of its principal's key over a challenge the verifier chose. Over MOQT the relay's SETUP carries the challenge (GRANT_CHALLENGE, 0x3A47) and the client's the grant as an AUTHORIZATION TOKEN beside the proof (GRANT_PROOF, 0x3A49), with the context SETUP //<authority><path>. check_proof is §10.5.4's checks, given the grant's digest and principal by whatever read the grant; over HTTP the same record rides the WAI-Room-Proof header and the challenge WAI-Room-Challenge.

The receipt extension is declared by the Setup Option 0x3A46, a varint holding the extension's version. -22 has no unregistered range for Setup Options and asks for a provisional registration; until one is made the code point is this crate's choice, and a peer that does not know it ignores it (§9.1).

Not yet implemented

  • WebTransport (MOQT over HTTP/3, §6.2.1), a relay with priority fan-out and per-object receipts, and the authorization token cache (§8.9) are in joule-moq-relay, built on this codec; this crate's own session keeps no token cache. The browser client is joule-moq-web, over this crate built without its default feature runtime (the codec, the carriage and the receipts, for wasm32-unknown-unknown).
  • Evaluating filters at a publisher (§3.3), and holding a peer to its MAX_FILTER_RANGES: the codec reads and writes every filter and counts their ranges.
  • GOAWAY, delivery-timeout enforcement on real streams and §5.1's scheduling are joule-moq-relay's; this crate's QUIC transport has none of them, and its mock relay applies delivery timeouts on a caller-driven clock.
  • draft-ietf-moq-loc and draft-ietf-moq-msf semantics (catalogs, LOC properties): their properties ride as any property does.

The vector corpus

tests/vectors/draft-22/vectors.json holds -22 vectors: varints, every control message type, control, subgroup, fetch and padding streams, datagrams, and the WAI_RECEIPT value; and negative vectors, each with the refusal it must meet. tests/draft22_vectors.rs generates the file deterministically from typed values and holds the crate to it; JOULE_MOQ_WRITE_VECTORS=1 rewrites it. tests/vectors/draft-22/moqt22_check.py decodes the corpus independently, from the draft's text, standard library only:

python3 -I tests/vectors/draft-22/moqt22_check.py

Each vector has a name, a kind, its bytes as hex, canonical (whether this crate's encoder makes exactly those bytes) and expect; a uni-stream vector also has context.group_order (1 Ascending, 2 Descending), the Group Order of the request a fetch stream answers.

kind bytes reading
varint one varint {"value", "length"}
control one framed message the message (below)
uni-stream a whole stream to its FIN a list of events (below)
datagram one datagram {"datagram": {…}} or {"padding": n}
wai-receipt the WAI_RECEIPT value its five fields

The JSON view: byte strings are lowercase hex; a Track Namespace is a list of its fields in hex; a Location is [group, object]; a Key-Value-Pair is {"type", "value"} for an even type and {"type", "bytes"} for an odd one, with Immutable Properties as raw bytes. A message is an object with "message" (its name in Table 5) and its fields by their names in the draft, lowercased with underscores. A parameter is {"type": NAME} with "value" (a varint or a uint8), "token", "location", "filter" (a Location Filter's filter_type and the fields it has; a Range Filter's set_id, property_type for the property filters, and ranges as [start, end or null]; null for Length 0), "parameters" (FILL_PARAMETERS) or "namespace". Stream events are {"header": "subgroup" | "fetch" | "control" | "padding", …}, {"control": message}, {"object": …} (subgroup), {"fetch_object": …}, {"end_of_range": {"kind", "location"}} and {"padding": n}.

A refusal is {"error": kind, "consequence": c}, with c one of session:<code>, request:<code> or malformed-track (and property_invalid for the WAI_RECEIPT value, which a sink discards). The kinds are WireError::kind: truncated, trailing-bytes, unknown-message-type, unknown-stream-type, invalid-flags, unknown-parameter, duplicate, parameter-not-allowed, duplicate-filter, invalid-filter, type-overflow, length-exceeds-limit, too-many-namespace-fields, empty-namespace-field, invalid-value, overflow, properties-on-non-normal, unknown-object-status, no-prior-object, empty-datagram-properties, malformed-token, unexpected-message, malformed-track.

Tests

  • cargo test -p joule-moq --features quic --all-targets runs the codec's unit tests, the corpus (tests/draft22_vectors.rs), generated round trips and hostile input (tests/draft22_roundtrip.rs), the QUIC transport over loopback, the mock and relay, the staged MoQ trace and WAI's JWP corpus.
  • The independent decoder above runs over the corpus in CI (.github/workflows/joule-code-conformance.yml).