The runner's test cases and WAI receipts end to end against each relay the interoperability runner's registry lists, at each draft both speak, over QUIC and WebTransport; and other implementations' test clients against joule-moq-relay. Relays and clients are named by what they offer; each result that is not a pass names its cause. Co-Authored-By: Claude Opus 5.5 <[email protected]>
349 lines
24 KiB
Markdown
349 lines
24 KiB
Markdown
# 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 drafts it implements
|
|
|
|
`draft-ietf-moq-transport-22` (1 October 2026), the default, and two earlier drafts
|
|
for interoperation with deployed relays and other implementations:
|
|
`draft-ietf-moq-transport-18` (12 May 2026), the working group's interoperability
|
|
target, and `draft-ietf-moq-transport-16` (13 January 2026). A draft is chosen before
|
|
the first MOQT byte, by the ALPN `moqt-NN` on native QUIC or the `WT-Available-Protocols`
|
|
value `moqt-NN` on WebTransport; this crate prefers -22, then -18, then -16
|
|
(`draft::Draft`). Drafts do not interoperate with each other, so a session is one draft
|
|
from its first byte to its last. `MOQT_DRAFT_VERSION` is -22's conventional code,
|
|
`0xff000016`.
|
|
|
|
Each draft is a codec module of its own: `draft22`, `draft18`, `draft16`. The rest of
|
|
the crate (tracks, objects, receipts, the session's state) is written once, in -22's
|
|
model; `draft18::bridge` and `draft16::bridge` map every message and object of the
|
|
earlier drafts to and from it, and name what one draft can say and the other cannot.
|
|
`joule-moq-relay`'s `transport::bridge` runs a whole session that way, so the relay and
|
|
its client speak all three drafts with one implementation.
|
|
|
|
### Where -18 differs from -22
|
|
|
|
-18 has -22's integer, Key-Value-Pairs, names, Token structure, message frame, a pair of
|
|
unidirectional control streams and a bidirectional stream per request, and -22's objects
|
|
on the wire; `draft18` uses -22's code for those and has its own for the rest:
|
|
|
|
| | -18 | -22 | the bridge |
|
|
|---|---|---|---|
|
|
| filter parameter `0x21` | SUBSCRIPTION_FILTER, length-prefixed: Next Group Start, Largest Object, AbsoluteStart, AbsoluteRange (End Group Delta) | LOCATION_FILTER, six forms | Relative Start 0, Next Object, Absolute Start, Absolute Start Group End; -22's other forms are refused |
|
|
| FILL_PARAMETERS, Range Filters, INCLUDE_PROPERTIES | none | defined | refused (NOT_SUPPORTED) |
|
|
| PUBLISH_OK's parameters | the subscription's (filter, forward, priority, group order, timeouts) | EXPIRES only; the rest by REQUEST_UPDATE | left out, by name |
|
|
| GOAWAY | a Request ID, on the control stream | none | supplied from the peer's Request IDs seen |
|
|
| FETCH | Fetch Type: Standalone (End exclusive, `{g, 0}` the whole group) or Joining | namespace, name, LOCATION_FILTER (inclusive); joining by fill | exclusive to inclusive; Joining refused |
|
|
| FETCH_OK End Location | exclusive | inclusive | `{g, o}` to `{g, o-1}`, `{g, 0}` to `{g, 2^64-1}` |
|
|
| PUBLISH_BLOCKED / PUBLISH_SKIPPED | 0x0F | 0x0F | renamed |
|
|
| PUBLISH_STATE_NOTIFY | none | 0x22 | left out |
|
|
| PUBLISH_DONE Stream Count, unknown | 2^62-1 | 2^64-1 | mapped |
|
|
| End of Timed-Out Range (0x20C) | none | defined | sent as End of Unknown Range |
|
|
| Setup Options 0x06, 0x08 | unknown | MAX_FILTER_RANGES, MAX_REQUEST_UPDATES | left out both ways |
|
|
| codes | VERSION_NEGOTIATION_FAILED, DUPLICATE_SUBSCRIPTION, INVALID_JOINING_REQUEST_ID, SUBSCRIPTION_ENDED | TOO_MANY_REQUEST_UPDATES, CONFLICTING_FILTERS, INVALID_FILTER | by number; an unknown code reads as INTERNAL_ERROR |
|
|
| a requester's FIN on PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE, SUBSCRIBE_TRACKS | cancels it (§6.1: "closing the stream with either a FIN or RESET_STREAM"; §6.2: withdrawn by cancelling) | not a cancellation (§6.4.2.2) | an -18 FIN reaches -22 as a cancellation; a -22 requester's FIN on these is not sent |
|
|
|
|
-18 itself leaves two things open, read here as follows: Table 5 lists `0x1E PUBLISH_OK`
|
|
pointing to REQUEST_OK, while §10.5 and the change log make PUBLISH_OK the name of the
|
|
REQUEST_OK that answers a PUBLISH; this crate sends 0x07 and reads either. And §2.5 names
|
|
`0x38`-`0x3F` as the one-byte application property range where its §15.8 (and -22) give
|
|
`0x78`-`0x7F`; this crate's own properties are in `0x3800`-`0x3FFF`, which both give.
|
|
|
|
### Where -16 differs from -22
|
|
|
|
-16 writes every integer as QUIC's (RFC 9000 §16: 1, 2, 4 or 8 bytes, at most 2^62-1),
|
|
and has another session shape, so `draft16` is its own throughout:
|
|
|
|
| | -16 | -22 |
|
|
|---|---|---|
|
|
| control | one bidirectional stream the client opens: CLIENT_SETUP (0x20), SERVER_SETUP (0x21), then every request and response by Request ID | SETUP (0x2F00) on each end's unidirectional stream; a stream per request |
|
|
| Request IDs | the next in sequence (§9.1), within MAX_REQUEST_ID (setup parameter 0x02, message 0x15), REQUESTS_BLOCKED (0x1A) | parity and uniqueness |
|
|
| cancelling | UNSUBSCRIBE, FETCH_CANCEL, PUBLISH_NAMESPACE_DONE, PUBLISH_NAMESPACE_CANCEL | resetting the request's stream |
|
|
| Message Parameters | Key-Value-Pairs: priority, group order and forward as integers, LARGEST_OBJECT length-prefixed; one DELIVERY_TIMEOUT (> 0) | each in its own form; two delivery timeouts |
|
|
| SUBSCRIPTION_FILTER AbsoluteRange | End Group absolute | (-18) a delta |
|
|
| SUBSCRIBE_NAMESPACE (0x11) | Subscribe Options: PUBLISH, NAMESPACE or both | SUBSCRIBE_NAMESPACE and SUBSCRIBE_TRACKS |
|
|
| SUBGROUP_HEADER | no FIRST_OBJECT bit | FIRST_OBJECT 0x40 |
|
|
| fetch objects | Group and Object IDs absolute | delta-coded in the Group Order |
|
|
| padding | none | streams and datagrams |
|
|
| a Track Namespace | 1 to 32 fields | 0 to 32 |
|
|
| PUBLISH_DONE codes | EXPIRED 0x5, TOO_FAR_BEHIND 0x6 | the reverse |
|
|
| stream reset UNKNOWN_OBJECT_STATUS | 0x4 | 0x6 (0x4 is GOING_AWAY) |
|
|
|
|
`joule-moq-relay`'s bridge renumbers this end's Request IDs into -16's sequence, grants
|
|
the peer requests ahead of it, routes each request and its responses to a stream of its
|
|
own, turns a cancelled stream into -16's cancel message, and re-encodes every object (the
|
|
pairs inside Immutable Extensions included). A request that finds no Request ID left under
|
|
the peer's MAX_REQUEST_ID for ten seconds is refused locally, with EXCESSIVE_LOAD. -16's
|
|
SUBSCRIBE_NAMESPACE rides a bidirectional stream of its own (§9.25) while every other
|
|
request rides the control stream, and QUIC does not order one stream against another, so
|
|
a peer's request can arrive ahead of an earlier one still in flight: the bridge admits a
|
|
new ID of the peer's parity within the grant, leaving the gap, and closes the session with
|
|
INVALID_REQUEST_ID for an ID seen before, where §9.1 would close it for any ID out of
|
|
sequence.
|
|
|
|
## 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
|
|
|
|
- `draft-ietf-moq-transport-14` (ALPN `moq-00`, the version negotiated inside the setup
|
|
messages) and the drafts between -16 and -22 other than -18. A relay that offers only
|
|
-14 is not reached.
|
|
- 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:
|
|
|
|
```sh
|
|
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`, and `session-rule` (a session error with a code of its own, such as
|
|
KEY_VALUE_FORMATTING_ERROR).
|
|
|
|
### The -18 and -16 corpora
|
|
|
|
`tests/vectors/draft-18/vectors.json` and `tests/vectors/draft-16/vectors.json` hold
|
|
each earlier draft's vectors in the same form, generated by `tests/draft18_vectors.rs`
|
|
and `tests/draft16_vectors.rs` (`JOULE_MOQ_WRITE_VECTORS=1` rewrites them) and decoded
|
|
independently by `tests/vectors/draft-18/moqt18_check.py` and
|
|
`tests/vectors/draft-16/moqt16_check.py`, each written from its draft's text:
|
|
|
|
- every control message of the draft, every parameter it may carry, each subscription
|
|
filter, each FETCH type, and the setup messages;
|
|
- the drafts' own examples: -18 §1.4.1 Table 2's integers; RFC 9000 Appendix A.1's for
|
|
-16's integer; and the first subgroup of -18 §11.6 and -16 §10.5. Their second
|
|
example cannot be reproduced: it gives a Properties Length of 33 and a Length of 21
|
|
for a 14-byte value. 33 is what -16's integer would take for a 21-byte value, not
|
|
-18's. The working group publishes no vector corpus of its own;
|
|
- control, subgroup (Immutable Properties, End of Group), fetch (both orders at -18;
|
|
absolute IDs at -16) and padding streams, and datagrams;
|
|
- negative vectors for what each draft refuses that a later one reads (a -22 form in an
|
|
-18 message, End of Timed-Out Range, -22's SETUP in -16, LARGEST_OBJECT without its
|
|
length, a DELIVERY_TIMEOUT of 0, FIRST_OBJECT at -16, and more), each with the
|
|
draft's consequence.
|
|
|
|
The -18 view differs from -22's in its filter (`{"filter_type", "start", "end_group_delta"}`),
|
|
FETCH (`fetch_type` and the Standalone or Joining fields) and GOAWAY (`request_id`, null
|
|
when absent); the -16 view adds each response's `request_id`, calls Properties
|
|
`extensions`, and writes an AbsoluteRange's `end_group`.
|
|
|
|
## Tests
|
|
|
|
- `cargo test -p joule-moq --features quic --all-targets` runs the codecs' unit
|
|
tests (each draft's, and each bridge's mappings both ways), the three corpora
|
|
(`tests/draft22_vectors.rs`, `tests/draft18_vectors.rs`, `tests/draft16_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 decoders above run over their corpora in CI
|
|
(`.github/workflows/joule-code-conformance.yml`).
|
|
- `joule-moq-relay`'s `tests/drafts.rs` runs sessions at -18 and -16 through the relay
|
|
(see that crate), and `joule-moq-interop` runs this crate against other
|
|
implementations' relays (below).
|
|
|
|
## Interoperability
|
|
|
|
Runs of 2026-10-21, from the build host, with `joule-moq-interop` (in this workspace) and
|
|
`joule-moq-relay` at this revision. Relays and clients are named by what they offer; the
|
|
interoperability runner's registry (its `implementations.json` at commit `e8d544e9`, of
|
|
2026-10-20) lists their endpoints, and `scripts/public.py` reads it.
|
|
|
|
Two scenarios per relay, draft and transport:
|
|
|
|
- **the runner's test cases**: `setup-only`, `announce-only`, `publish-namespace-done`,
|
|
`subscribe-namespace-lifecycle`, `subscribe-error`, `rendezvous-timeout` (none at -16,
|
|
which has no RENDEZVOUS_TIMEOUT), `announce-subscribe`, `subscribe-before-announce`;
|
|
- **WAI receipts end to end** (`wai-receipts`): one session publishes a catalog, a WAI
|
|
track (ten groups of three objects, each a WAI envelope carrying its receipt in Immutable
|
|
Properties) and its receipts track through the relay; a second session subscribes and
|
|
verifies every object against its receipt: inclusion, the signed bundle, the receipts
|
|
chain, and the receipt and envelope the object carries. Where it does not pass, it runs
|
|
again by PUBLISH instead of the namespace route, and with no per-object properties, to
|
|
find which part the relay does not carry.
|
|
|
|
**Pass** is every case, both scenarios, on every transport tried; **partial** names what
|
|
did not pass and why; **fail** is a scenario the relay does not carry.
|
|
|
|
| Relay | Draft | QUIC | WebTransport | Result | Cause of what did not pass |
|
|
|---|---|---|---|---|---|
|
|
| A public MOQT relay network's draft-18 interop endpoint | -18 | yes | yes | pass | |
|
|
| The same network's production endpoint | -16 | | | not covered | it needs an operator's token |
|
|
| The same network's draft-14 endpoint | -14 | | | not covered | this crate has no -14 |
|
|
| An open interop relay, drafts -14 to -22 on one port | -22, -18, -16 | yes | yes | pass | |
|
|
| A research relay at -18 | -18 | yes | yes | pass | |
|
|
| The same research relay at -22 | -22 | yes | yes | pass | |
|
|
| A relay at -22 | -22 | yes | yes | pass | |
|
|
| The same project's relay at -18 | -18 | yes | yes | fail | PUBLISH_NAMESPACE is accepted, but a SUBSCRIBE for a track in it gets no answer, nor does one after PUBLISH: `announce-subscribe` and receipts fail; no NAMESPACE for a subscribed prefix; `rendezvous-timeout`: DOES_NOT_EXIST. |
|
|
| A relay at -16 and -18 | -18, -16 | yes | yes | pass | |
|
|
| A relay at -14 to -22 | -22, -18, -16 | yes | yes | pass | |
|
|
| A media server's relay at -22 | -22 | yes | yes | partial | `rendezvous-timeout`: DOES_NOT_EXIST at once, not TIMEOUT after the 500 ms asked. Receipts pass. |
|
|
| A relay library's relay, -16 to -22 | -22, -18, -16 | yes | yes | partial | Receipts pass at every draft. A namespace a closed session published stays registered, so the next session's PUBLISH_NAMESPACE of it is refused ("Namespace already published"): `announce-only`, `publish-namespace-done`, `announce-subscribe`, `subscribe-before-announce` over QUIC at -22 and -18, `publish-namespace-done` over WebTransport. |
|
|
| An open relay network's anonymous endpoint, -14 to -20 | -18, -16 | yes | yes | partial | Receipts: every object delivered and verified against the receipts track, but the relay drops Object Properties, so no object carries its own receipt or envelope; it refuses PUBLISH (NOT_SUPPORTED). `rendezvous-timeout` at -18: DOES_NOT_EXIST ("unroutable"). Once, over WebTransport, an internal transport error answered a SUBSCRIBE. Every runner case passes at -16. |
|
|
| A relay at -18 to -22 | -18 | yes | yes | partial | As the row above: Object Properties dropped, PUBLISH refused, `rendezvous-timeout` DOES_NOT_EXIST. |
|
|
| The same relay at -22 | -22 | yes | yes | fail | It refuses the publisher's SUBSCRIBE_OK with MALFORMED_TRACK ("short buffer"), so no track crosses it: `announce-subscribe` and receipts fail. The same exchange passes there at -18, and at -22 with the relays above. |
|
|
| A relay at -18 with a self-signed certificate | -18 | yes | yes | partial | Objects that carry Object Properties are not forwarded: no media object arrives, while the receipts track (no properties) does and verifies; with no per-object properties every object verifies. SUBSCRIBE_OK for a track no one publishes (`subscribe-error`, `rendezvous-timeout`). |
|
|
| A relay at -18 over WebTransport only | -18 | | yes | fail | Not reached from the build host: no answer to the QUIC handshake. |
|
|
|
|
Other implementations' interoperability test clients against `joule-moq-relay` on
|
|
loopback (`scripts/clients.sh`), three rounds each, over QUIC and WebTransport:
|
|
|
|
| Client | Draft | Result | Cause |
|
|
|---|---|---|---|
|
|
| The public relay network's -18 test client (the runner's cases and its data-plane cases) | -18 | pass | |
|
|
| An open relay network's test client, its draft pinned | -18, -16 | pass | |
|
|
| The same client at its default draft | -22 | partial | Its SUBSCRIBE carries Message Parameter 0x1, which -22 does not define, so the relay closes the session (-22 §9.20: PROTOCOL_VIOLATION): `announce-subscribe`. It withdraws a namespace by a FIN, which at -22 is not a cancellation (§6.4.2.2): `subscribe-namespace-lifecycle`. |
|
|
|
|
These runs found five faults in `joule-moq-relay`, fixed here (its README states the
|
|
rules now): a PUBLISH_DONE that arrives before the streams it counts; a track published
|
|
again after TRACK_ENDED; a publisher that closes its control stream before its last
|
|
objects; an -18 requester's FIN on a namespace request; a -16 Request ID that arrives
|
|
ahead of one in flight on another stream. They also found one in the client: it stopped
|
|
reading after a count of streams, not of objects. `scripts/local.sh` runs the client
|
|
against the relay at each draft and across drafts, and checks that `--tamper` (a payload, a
|
|
receipt, or a byte of an object in flight) fails. None of these runs is in CI: they reach
|
|
the public internet or run other implementations' code.
|