Skip to content

fix(net)!: a transcode derivative is no longer sometimes unroutable - #8827

Draft
kixelated wants to merge 4 commits into
mainfrom
quest/m1/transcode-unroutable
Draft

kixelated wants to merge 4 commits into
mainfrom
quest/m1/transcode-unroutable

Conversation

@kixelated

@kixelated kixelated commented Nov 15, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m1/transcode-unroutable.md, which this PR deletes.

Summary

A viewer subscribing to src.hang/transcode.hang was sometimes refused with unroutable.

A claim covers every path beneath its prefix, so the source's publisher claims src.hang, and so also src.hang/transcode.hang, which it refuses. An announce stream answering a request for exactly src.hang/transcode.hang presents the most specific claim at or above the requested prefix at the requested prefix itself. The derivative's own claim and the source's covering claim therefore reached the viewer as two claims for the same path, ranked alike. Nothing but the order the relay listed them in, by node id, then decided which one the viewer asked, and a node id changes on every run. When the source's claim won, its publisher answered unroutable.

The draft already says the more specific claim wins "at any cost". The wire lost the specificity, so the receiver could not apply it.

  • Rust (moq-net): ANNOUNCE_START, ANNOUNCE_UPDATE and ANNOUNCE_RESTART carry Above, how many segments broader than the requested prefix the claim is. A stream presenting a broader claim at a narrower prefix adds the distance (route::Item::lowered), and route_order ranks the lowest Above first. A receiver refuses (PROTOCOL_VIOLATION) an Above on a claim that is not at the requested prefix, or larger than the claim's path has segments.
  • JS (js/net): the same codec, the same lowering in floors.ts and internal.ts presented, the same validation in the subscriber, the same leading comparison in compareRouteCandidates.
  • Draft: the three messages, the presentation paragraph, Resolution, and the changelog.
  • Tests: an integration test reproduces it at the session level (rs/moq-net/tests/integration/nested_claim.rs, which fails without the fix), the pinned announce bytes, the shared corpus and golden streams moved with the codec, and Above has unit tests in Rust and JS.

Public API and wire impact

  • Wire: moq-lite-07-wip only. ANNOUNCE_START, ANNOUNCE_UPDATE and ANNOUNCE_RESTART gain a varint Above after the Announce Cost and before the optional Lite-06 Chain. Earlier versions are unchanged.
  • Rust API: nothing public. route::Item and Lane.above are crate-private.
  • JS API: the @internal Claim type and WireRoute (local ranking state) gain above; the announce codec's AnnounceBroadcast gains an optional above.
  • Migration (lite-07-wip peers): both ends of a moq-lite-07-wip session must run this change, since the extra varint shifts the rest of the message. The wip version is unpublished and opt-in, so no published draft or release breaks.

Models

Size: S
Implementer: Claude, claude-sonnet-5-5, unknown effort (arm B)

Validation

  • Reproduced on the real binaries first, from the quest's recipe (loopback relay with test/portrait/relay.toml, ffmpeg into moq import avc3, moq transcode --encoder software --decoder software, then two moq export viewers at src.hang/transcode.hang): the relay logged unroutable in about 60% of runs; with each side's trace logs the viewer asked the source's node for the derivative whenever the source's node id sorted ahead.
  • The same recipe on this branch's moq-relay and moq (built with transcode,openh264): 8 of 8 runs delivered video to both viewers, with no unroutable in the relay or either viewer log.
  • nested_claim::a_nested_broadcast_wins_over_the_claim_above_it runs a source, a derivative, a relay and a viewer over lite-07 sessions, sweeps both node-id orderings, and asserts the viewer reads the derivative's frame. It failed on the pre-fix code and passes now.
  • cargo nextest run -p moq-net --all-features: 3992 passed.
  • just check: passed (Rust, JS 3344 bun test, biome, drafts, quest).
  • just test interop --all: all checks passed, including the JS to Rust compress, drop and claims runs on moq-lite-07-wip and the Python sockets.

Caused by

  • moq-dev/moq#8293 - presented a covering claim at the requested prefix without saying how far above it was, so it tied an exact claim (found by person).

Decisions

Choices made without the maintainer, each one a review can undo:

  1. Fix at the wire, not in the relay's tie-break. The receiver cannot recover how broad a presented claim was from the claim alone, and a tie-break on node id or age would still pick the wrong one half the time. The alternative is presenting every covering claim at its own depth, which costs one entry per ancestor on every stream.
  2. Above leads route_order, ahead of drain, availability, priority and cost. The draft says an exact claim shadows broader ones "at any cost", so a cheaper or non-draining broader claim must not win. The alternative is ranking it after drain and availability, so a draining exact claim loses to a live broader one that then refuses the path.
  3. Above is always present on a lite-07 message, zero for a claim at or inside the prefix, not an optional field. An optional field would need a flag, and a flag needs a second code path in every codec.
  4. A receiver closes the session on an Above that cannot be true, instead of clamping it, as it does for other wire fields the peer fixes (Announce ID and Route ID).
  5. The crate-private Lane.above is a u8 (a path has at most 32 segments), so the route entry stays inside clippy's large_enum_variant budget in ietf/publisher.rs. The alternative is boxing NamespaceWork::Apply.
  6. The title carries ! since the wire changes, though only for the unpublished lite-07-wip.

Follow-ups

None.

🤖 Generated with Claude Code

(Written by Claude Sonnet 5.5)

Review

Rewritten by quest review after each round.

  • Head: 6959dd1, named in full once no reviewer reports a finding

  • Base: origin/main at 6656f32

  • Risk: high, The wire-protocol routing changes affect interoperability for publishers and relays; reverting restores the implementation but cannot undo interrupted sessions or misrouted requests.

  • Classified: medium by default

  • Reviewer: Codex, gpt-6.1-sol, medium effort

  • Session: 01a1dfcc-ae6a-72f0-8060-2538ba7293b1, round 3

  • Score: 8/10, The Rust refusal fallback is fixed, but the JS counterpart still allows a broader claim to supersede an exact broadcast through epoch comparison.

  • Raised the risk to high: The wire-protocol routing changes affect interoperability for publishers and relays; reverting restores the implementation but cannot undo interrupted sessions or misrouted requests.

  • Rounds: 3; first round 3 (high 1, medium 2); findings 5 (high 1, medium 4): fixed 4, refuted 0, open 1

  • Findings, open until the next round settles them:

    • Medium, js/net/src/origin.ts:1186: Apply specificity before epoch supersession
  • The last round's findings, at 2cd6abd:

    • Fixed: medium, rs/moq-net/src/model/origin.rs:6110: Restrict serving candidates to the winning specificity tier; RouteTable::best_route now restricts serving, epoch supersession, and ranking to candidates matching the winning specificity, so a refusing exact broadcast cannot fall through to a broader claim.
  • Reviewer: Codex, gpt-6.1-sol, low effort, shadow

  • Session: 01a1dfcc-ae6a-7830-9841-a9783fb5b782

  • Rounds: 2; first round 1 (medium 1); findings 1 (medium 1): fixed 1, refuted 0, open 0

kixelated and others added 2 commits November 15, 2026 04:40
A claim covers every path beneath its prefix, and an announce stream presents the most specific claim at or above the requested prefix at that prefix itself. A source's covering claim for `src` and a derivative's own claim for `src/derived` therefore looked alike to a viewer requesting `src/derived`, and the order the relay listed them in, by node id, decided which one it asked. The source's publisher refuses a path it does not serve, so a viewer reached the derivative on some runs and got `unroutable` on others.

ANNOUNCE_START, ANNOUNCE_UPDATE and ANNOUNCE_RESTART carry `Above`: how many segments broader than the requested prefix the claim is. A stream presenting a broader claim at a narrower prefix adds the distance, and a receiver ranks the lowest `Above` first, so a claim exactly at the path shadows every broader one at any cost, as the draft says. A receiver refuses an `Above` on a claim that is not at the requested prefix, or larger than its path has segments.

The wire change is to the unpublished moq-lite-07-wip, mirrored in js/net and the draft.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title A transcode derivative is sometimes unroutable fix(net)!: a transcode derivative is no longer sometimes unroutable Nov 15, 2026
kixelated and others added 2 commits November 15, 2026 08:06
…prefix

A claim shown above its own prefix is filed deeper than it is, so a broad claim shown at a deep prefix could still beat a more specific claim filed shallower. Selection now ranks by the claim's own depth (the filed prefix less Above) across every covering prefix, in Rust and JS.

A relay forwards a claim it learned from a narrower stream at a deeper suffix of a broader request, so a receiver accepts Above on any claim, up to the path's segments, instead of only at the requested prefix. A publisher holds Above to what its handle's root leaves of the path, so a claim above a rooted export is no broader than the peer's path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A node's most specific claims are its whole answer: the broadcast that refuses a path no longer falls through to a broader claim shown at the same prefix. Rust restricts the serving candidates to the winning tier, and JS's local-wins check lets an exact broadcast beat a broader received claim.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant