diff --git a/quest/m0/README.md b/quest/m0/README.md index f704c9a3310..aab30dd1c65 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -5,17 +5,13 @@ The work in flight now. Relay hardening: legal moq-transport input never fails a session in current interop coverage, every resource a peer can make the relay hold is bounded by what it sent or by a budget, and no peer input panics -the process. Identity: nothing treats who published a route as what it -carries; a path and the epoch on its route are the only content identity, and -every first-party publisher that can restart mints a fresh epoch, so #4741 -stalls nobody. Alongside them: fixes for the `release` line, CI cost, and the +the process. Alongside it: fixes for the `release` line, CI cost, and the recurring audits. ## Plan The release API gates (#3829..#3878) and the release that followed them are -done. moq.pro pins this repository's `release` line, so the release gate below -also keeps #4741 from reaching it early. +done. Per-session request caps landed in #4820, idle fronts in #5054, and the lost demand-poll wake in #5091. The rest of the 2026-09-29 DoS review remains @@ -26,13 +22,6 @@ serve instead of enumerating broadcasts. Serving the relay's ingested-only view (`origin::Consumer::local()`) to localhost workers belongs to moq.pro's edge, which embeds moq-relay; it moved there on 2026-09-28. -Identity: the [broadcast epoch](/quest/m0/broadcast-epoch/README.md) line -gates the next release (decided 2026-10-03: -#4741 resumes an un-epoched republish into the old broadcast and stalls its -viewers). #4741 is on main, but no release ships until first-party -publishers mint epochs. Backport patches cut from `release` don't carry -#4741 and aren't gated (decided 2026-10-08). - Hygiene (decided 2026-10-19): the audits sit at the end of this list. Each recurs once per major release: its PR adds the next run to the following milestone, gated from m2 onward on the [release cut](/quest/m2/release/cut.md). @@ -40,7 +29,6 @@ Each lands its fixes in its own PR and plans only what needs a decision. ## Required -- [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - every first-party publisher that can restart mints a fresh route epoch, the newest wins a path, and only routes with the same epoch resume a subscription - [64-bit Hop IDs](/quest/m0/hop-id-64.md) - a moq-transport peer with any non-zero 64-bit Hop ID keeps its session, and Hop IDs are `u64` in Rust and JS - [MALFORMED_AUTH_TOKEN session code](/quest/m0/malformed-auth-token-code.md) - a 0x16 close reports a malformed token instead of `unknown code=22`, and moq-net sends it for an unparseable token - [Release shutdown backport](/quest/m0/ffi-shutdown-backport.md) - `release` publishes a non-breaking moq-ffi with an async `drain()` and qmux sessions on the QUIC stream limit diff --git a/quest/m0/broadcast-epoch/README.md b/quest/m0/broadcast-epoch/README.md deleted file mode 100644 index 23d6b6fde4a..00000000000 --- a/quest/m0/broadcast-epoch/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# [L] Broadcast epochs - -## Goal - -A path and the epoch on its route are the only content identity, and no -first-party publisher reuses a pair for different content. Only routes with -the same epoch resume a subscription from the first frame it lacks, within -the subscriber's max age; a route without one keeps its subscriptions until -it goes. So epochs are what make -failover seamless. Routes without an epoch never splice (maintainer, -2026-10-08): a `Restart` (or an end and start on older versions) tells every -downstream subscriber to stop using its copy of the old source and -resubscribe fresh; a downstream relay retires its copy for new requests and -forwards the `Restart`. A restart is a new epoch at the same -path: the newest epoch wins new requests and announce consumers see that -`Restart`, so viewers re-request rather than stall on a replaced -broadcast. Subscriptions already on the old one stay until the -application drops them or its route goes. Without an epoch, a publisher -that restarts on a relay while its old session lingers there wins at once, -whatever its new hop: the relay ranks its own sessions' routes newest first -and hashes only peer routes. Through a peer, a restart on a different chain -of the same length and cost that loses the routing hash is reached only once -the old session closes and its route is withdrawn. - -The epoch rides moq-lite 07 announcements and requests as metadata, and the -negotiated IETF epoch extension carries it on supported moq-transport versions. -The path never changes. Versions without epoch support keep working: their -routes carry no epoch, and see a restart as an end and start at the same path. - -Decided 2026-10-10: keep epochless clients supported on updated relays, with -unchanged paths and End+Start on replacement. The relay retains the explicit -identity internally; fresh requests cannot join its old cached instance. -This does not promise cache invalidation in arbitrary older IETF relays or -clients that retain objects across broadcasts. The negotiated -[IETF extension](/drafts/draft-lcurley-moq-epoch.md) landed in m1, independently of -this release gate; seamless JS handover stays in m1 too. - -Non-goals: pooling, which needs nothing here. A redundant ingest pair runs on -distinct epochs (decided 2026-10-19), so only replicas of one publisher's -bytes share one. -Also out of scope: trusting the publisher's clock (a far-future epoch wins -until its route goes away). - -## Plan - -Decided: - -- This line gates the next release (decided 2026-10-03: without epochs every - restarting first-party publisher stalls its viewers). -- The epoch is route metadata, not a path segment (decided 2026-10-06, - replacing the `@` segment: a path suffix changes the name old - clients subscribe to). It is a lite-07 field on ANNOUNCE_START, TRACK, - SUBSCRIBE, and FETCH, with no negotiation and nothing on published versions. -- The route's epoch is taken as given: nothing mints one by default (decided - 2026-10-06). Each first-party publisher mints one per run and announces it; - a replica announces a shared one. A route without one, such as a - transcoder's prefix claim, is never stitched to another worker's output. - A better route without an epoch wins new requests and is announced as a - `Restart`, replacing "stays on the worker that first served a - subscription", which let dead routes linger (decided 2026-10-07). -- The newest epoch wins a prefix ahead of cost (decided 2026-10-06). The - hard switch that ended subscriptions in flight with `Unroutable` (decided - 2026-10-06 for epochs, and 2026-10-07 in #5013 for routes without one) is - reversed (2026-10-07): subscriptions stay sticky on their route and an - explicit `Restart` announce event tells players to follow. When the newest goes and - an older one is still live, the older one wins again as a new broadcast. -- [Announcement takeover](/quest/m1/announce-takeover.md) stays m1 and gates - the lite-07 cut, not this release. It replaces the response-only claim-epoch - proposal: an independent route explicitly continues the same served - instance, rather than inferring continuity from an epochless claim. -- Decided 2026-10-14: announcement-following readers handle a referenced - sibling's replacement as Restart; other readers pin their first resolution - for the catalog instance's lifetime. Optional reference epochs constrain - resolution on epoch-aware transports and remain best effort on epochless - transports. Following never turns a replacement into seamless resume. The - schemas, shared resolution, compatibility documentation, and `moq_mux::follow` - (which `moqsrc` follows siblings with) landed. -- Every first-party publisher that can restart mints its own: the apps, - moq-boy, the ingest gateways, moqsink, OBS, and the bindings. Players - (`moq play`, `@moq/watch`, demo/web) follow the announce `Restart`. moq-stats mints one per - [group announcement](/doc/concept/stats.md#broadcasts), which also gated - the release (decided 2026-10-04). -- Until lite-07 is offered by default, default sessions (lite-06) carry no - epoch, so a cluster GOAWAY redial or a standby takeover ends subscriptions - instead of resuming them (accepted 2026-10-06 over negotiating the field on - lite-06). A release that needs seamless failover promotes lite-07 first. - -Decided 2026-10-06: older versions and moq-transport lose cross-route resume, -since their routes have no epoch, but their mid-group start handling stays. -The IETF joining FETCH is the normal live join for every IETF subscription, and -the IETF resume point and lite-05/06 `widen_frame_bounds` still serve any -mid-group start: a public `Subscription::with_start` or a downstream Frame -Start a relay forwards upstream. - -The end-to-end gate republishes a name under a new epoch while the old -publisher's session stays open, then closes the newest so the still-live older -one wins again. The real relay (the CLI integration suite's `epochs` cases) and -both real players (decided 2026-10-09, from #5154: `moq play` through `just test -native-play` and a browser `@moq/watch` through `just test media`) run it on -lite-07, lite-06, and IETF, holding A's catalog and media across B's takeover. - -Decided 2026-10-23 (maintainer, on -[PR 6548](https://github.com/moq-dev/moq/pull/6548)): the gate's "RTT-scale -bound" means announcement-following readers observe the replacement within the -protocol round trips plus the configured announcement hold, independent of the -idle timeout, and real players present it within those routing delays plus -their configured codec cadence, decoding, and playback delays. A player cannot -show a new run sooner than its keyframe cadence and playback delay allow, and -the hold is a deliberate routing setting, so neither is a stall; the gate -exists to exclude waiting on the old session's idle timeout. Each gate enforces -the bound derived from its own configuration rather than a hang watchdog. -That PR fixed the IETF22 viewer's group recovery and its object-stream error. -The gates landed with two problems left open (maintainer): a fallback session -drop and audio stall that did not reproduce, tracked in -[IETF22 fallback](/quest/m1/test-flakes-2/ietf22-fallback.md), and the native -player reopening its audio output on each switch, which glitches under load, -tracked in [Play keeps its audio output](/quest/m1/play-audio-handover.md). - -When the release cut carries stats epochs, drop -[#4810](https://github.com/moq-dev/moq/pull/4810)'s wall-clock group seed and -its `doc/concept/stats.md` sentence from `release`; until then, the -maintainer's pre-release merge of `release` into `main` keeps `main`'s -`rs/moq-stats` and `doc/concept/stats.md`. MoQ Pro's VOD `storage.json` moves from the same seed to its own epoch. - -The 2026-10-10 resume audit adds release gates for newly issued requests -through retired handles and for late duplicate delivery after same-epoch -handover. Keep existing reads sticky; stopping new unpinned wire work is a -separate boundary. Catalog references apply the same identity policy to -players and exporters, distinguishing announcement followers from readers -that retain lifetime pins. Wall-clock expiry, live DROP dispositions, -FETCH budgets and seamless JS handover remain m1; the IETF extension landed -independently. diff --git a/quest/m1/README.md b/quest/m1/README.md index 293cb64ec03..bead0a53e3e 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -84,7 +84,6 @@ audit's receive-memory, priority, and lease bugs rank ahead of that reshape. ## Required -- [Epoch correctness](/quest/m0/broadcast-epoch/README.md) - publisher identity, duplicate suppression, and catalog pinning gate the breaking cut - [moq-congestion](/quest/m1/congestion/README.md) - one QUIC congestion-control crate with a transport-independent trait, run by `moq-quic` and published for noq - [Release audit findings](/quest/m1/release-audit/README.md) - resolve newly identified resource, parser, playback, and interoperability risks before the cut - [SUBSCRIBE_TRACKS follows credit](/quest/m1/listing-credit.md) - a listing holds state in proportion to what the peer reads, not to the catalog diff --git a/quest/m2/e2ee/README.md b/quest/m2/e2ee/README.md index b48c73cf259..52e142e2fa4 100644 --- a/quest/m2/e2ee/README.md +++ b/quest/m2/e2ee/README.md @@ -21,7 +21,7 @@ The contract is [draft-lcurley-moq-e2ee](/drafts/draft-lcurley-moq-e2ee.md), pro - Every publisher instance mints an epoch, a UUIDv7 in lowercase text, and publishes at `` with the epoch on its route, where `` derives from the credential and the semantic broadcast name without the epoch. The epoch is an input to every HKDF derivation, so a restart, takeover, or explicit group sequence cannot repeat a nonce under a key: nothing is persisted across instances and no generation counter is redistributed. The epoch is the shared [`Epoch`](/doc/concept/moq-lite.md#publisher-epochs) primitive. Subscribers resolve `` and take the epoch from its route, where the newest wins; on a wire without route epochs the application supplies it. - An encrypting publisher refuses a shared or explicit epoch (maintainer, 2026-10-08). Two producers handed one epoch would derive the same keys and repeat every nonce. Both cores produce only under an epoch they minted themselves; consuming still binds a discovered one. Rejected: a per-instance salt. State the rule in the draft's epoch section. -- The epoch is untrusted and unauthenticated. A wrong epoch fails authentication and a withheld one denies service; neither can make a nonce repeat, because only the publisher instance chooses what it encrypts under. This is the same trust a cache needs to serve the right instance. Plaintext broadcasts adopt the same epoch by default through [broadcast epochs](/quest/m0/broadcast-epoch/README.md). +- The epoch is untrusted and unauthenticated. A wrong epoch fails authentication and a withheld one denies service; neither can make a nonce repeat, because only the publisher instance chooses what it encrypts under. This is the same trust a cache needs to serve the right instance. First-party plaintext publishers already mint the same route epoch. - Nothing in the path says the bytes are encrypted. The format after decryption (`meeting.hang`) is inside the opaque name; a plaintext player, exporter, or matcher that opens a protected broadcast finds no catalog it can read and fails with its usual typed refusal, the same as for any format it does not support. - One 32-byte secret authorizes the whole broadcast; HKDF-SHA-256 derives separate AES-128-GCM keys for each physical track and for grouped-frame versus datagram domains. A grouped frame uses the 96-bit nonce `uint64_be(group) || uint32_be(frame)`; a datagram uses its sequence with frame zero under the datagram domain. Empty AAD: every immutable end-to-end field is in the HKDF info or the nonce. - Within an instance, group and datagram sequences are allocated monotonically and frames are numbered in write order, which is the whole reuse rule. There is no ciphertext retention or retransmission API: relays forward bytes unchanged, and an application that needs to resend encrypts nothing twice because it never gets the same identity twice. diff --git a/quest/m2/release/cut.md b/quest/m2/release/cut.md index 687ddd5cdaf..2732883498d 100644 --- a/quest/m2/release/cut.md +++ b/quest/m2/release/cut.md @@ -18,6 +18,12 @@ tags follow this cut. This planning PR cuts and publishes nothing. After publication, re-measure the planned wire-compat breaks against the published versions, as CONTRIBUTING.md's release procedure describes. +This cut carries stats epochs, so it drops +[#4810](https://github.com/moq-dev/moq/pull/4810)'s release-only wall-clock +group seed: the merge of release into main keeps main's `rs/moq-stats` and +`doc/concept/stats.md`. MoQ Pro's VOD `storage.json` moves from the same +seed to its own epoch. + Recurs once per major release (decided 2026-10-19). The PR that finishes the cut shifts the milestones up, as the root [questline](/quest/README.md) describes. This epic's remaining publication and adoption quests move to m0, diff --git a/quest/m2/stats/README.md b/quest/m2/stats/README.md index b018002765c..2dec19e58d2 100644 --- a/quest/m2/stats/README.md +++ b/quest/m2/stats/README.md @@ -11,8 +11,7 @@ its own connection. A dashboard reads both the same way a publisher does. One shared model turns either report into a health verdict, and a bounded preflight run reports which media layer of a broadcast is broken. Stats and feedback cost nothing on the network unless someone subscribes. Not here: the -relay's `moq-stats` layout, which this line does not change (the -[broadcast epoch](/quest/m0/broadcast-epoch/README.md) line reshapes it); +relay's `moq-stats` layout, which this line does not change; clock synchronization; any requirement that a client report; and feedback as an input to billing, authorization, or route selection. diff --git a/quest/m2/stats/health.md b/quest/m2/stats/health.md index 155ed2ab86f..28a084eb42f 100644 --- a/quest/m2/stats/health.md +++ b/quest/m2/stats/health.md @@ -38,7 +38,7 @@ Guidance, to be settled while building: snapshots' interval. RTT and estimated rate remain gauges read as reported. A reset is detected by a counter decreasing or by the reporting broadcast's path or epoch changing - ([Broadcast epochs](/quest/m0/broadcast-epoch/README.md)); the sample then + ([Broadcasts](/doc/concept/stats.md#broadcasts)); the sample then starts over rather than going negative. - Container sections such as `mpegts` are excluded from the verdict; their counters are read by their own consumers @@ -64,4 +64,3 @@ Public API: new health types in `hang` and `@moq/hang`. Wire: none. ## Related - [QoS](/quest/m2/qos/README.md) - the relay's delivery counters, which a combined downstream verdict also reads -- [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - a new epoch is one way a counter reset shows