open-standards/wai/live-conformance/carousel_moq_fec_verify.py
Transaction Science a4583d567c
All checks were successful
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 14s
wai-key-proxy / does this change touch it (push) Successful in 14s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 25s
wai-app and Create / wai-app · does this change touch it (push) Successful in 25s
wai-audit collector and player integration / does this change touch it (push) Successful in 32s
WAI Studio / does this change touch it (push) Successful in 19s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m42s
wai-quantum studio and desktop / does this change touch it (push) Successful in 1m51s
wai-sink-swift native sink package / does this change touch it (push) Successful in 2m14s
wai-fleet service / does this change touch it (push) Successful in 2m53s
wai-ledger service / does this change touch it (push) Successful in 3m11s
wai-sink-android native sink library / does this change touch it (push) Successful in 3m7s
WAI operation catalogue / does this change touch it (push) Successful in 3m24s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 3m26s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m38s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 5m27s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 7m6s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 7m20s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 7m8s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 4m34s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 12s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 11s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 37s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 2m41s
wai-key-proxy / proxy · linux (push) Successful in 1m49s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m22s
wai-audit collector and player integration / collector · linux (push) Successful in 2m18s
wai-quantum for JavaScript / javascript · linux (push) Successful in 5m2s
wai-quantum studio and desktop / studio · browser (push) Successful in 5m54s
wai-ledger service / service · linux (push) Successful in 2m11s
wai-fleet service / service · linux (push) Successful in 4m2s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 9m50s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 12m24s
WAI Studio / Studio in WebKit · linux (push) Successful in 13m19s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 11m24s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m32s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 6m4s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 4m51s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 15m29s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 19m19s
wai release binaries build on Windows / the seven release binaries · windows (push) Successful in 17m45s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m5s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 56s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m6s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m14s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 6m29s
wai-fleet service / service · macos (push) Successful in 39s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m0s
wai-ledger service / service · macos (push) Successful in 1m3s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m25s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 5m34s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m12s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m51s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 6m17s
Four MoQ drafts mapped onto what WAI has: playout timing, the FEC catalog binding, MoQ files and V3C tracks
Each is a mapping with a reference module, a corpus and a second implementation written
from its text. None adds a record or changes a byte of an envelope, a receipt or a claim,
and none is wired into a relay, the room service or a page: each module is what a publisher
and a receiver call. Every draft was read at the latest revision in the IETF archive on 10
November 2026.

- Playout timing (draft-nandakumar-moq-tempo-00; co-watch §15, new). joule-moq-room's tempo
  module reads and writes the draft's CaptureTimestamp, PlayoutDelay and HopTime, and holds
  the instant they state to the room: it is the millisecond in which co-watch's controlled
  position enters the object, and a delay above the track's stated bound is refused whatever
  instant it gives. A change of delay the room decides is a pause and a play, or a seek, sent
  as any control. The catalog member wai_tempo (moq-streaming-format §4.4) carries the bound
  and the two catalog extensions the draft leaves to be defined. The draft's property types
  are unassigned; the module uses placeholders in MOQT's application range.
- The MoQ FEC catalog binding (draft-ramadan-moq-fec-catalog-00, -fec-core-00,
  -fec-block-codes-00; carousel §8.3, new). wai-rs's carousel::moq_fec binds a schedule's
  raptorq entries to the FEC instances a catalog declares under moq-object-v1, and back:
  an object a block, a pass a repair layer, symbols as symbol-track and repair objects. An
  entry binds only where everything carousel §6 derives from it holds of the block sent (the
  same symbols a pass, the same K'); a catalog is read failing closed; a rebuilt object is
  delivered only at its entry's Location, length and content hash. tests/carousel_moq_fec.rs
  writes the corpus and decodes every object with an independent implementation of RFC 6330,
  whose symbols for the block-codes draft's Appendix B are that appendix's.
- Multicast endpoints (draft-ramadan-moq-multicast-02; bearer-receipts §9.7, which read -00).
  At -02 an endpoint lists only mmtp tracks and their repair tracks, so no WAI track rides
  one of the draft's endpoints, and §9.7 now says so. What is implemented is the reading of a
  catalog's multicast member, each endpoint standing or ignored with a label; it joins nothing.
- MoQ files (draft-jennings-moq-file-05; live-recording §8.7, new). joule-moq's file module
  writes a track's objects as the draft's .moq and .dat files and reads them back byte for
  byte, properties and all, so a track and its receipts track verify from the files alone. A
  reader opens nothing outside its directory and refuses a file cut short.
- V3C tracks (draft-yuyou-moq-v3c-00; moq-streaming-format §4.5, new). joule-moq's v3c
  module splits a wai.pc.vpcc payload into the draft's component tracks and joins it back,
  with the catalog member wai_v3c. ISO/IEC 23090-5 was not read: the unit header is RFC 10034
  §4.3.1's copy, unit types 7 and above are refused, and the corpus holds opaque test bytes,
  no bitstream an encoder wrote.

Planted faults, each caught by the reference and, but where noted, by the second
implementation: a symbol of another object (offered as itself, in place of a block's own
symbol, and, in the test alone, as a repair symbol under a rewritten header); a truncated data
file and a truncated .moq file; a timing claim outside its bound (a delay past the track's
bound, and an instant one millisecond off the controlled position's). Writing the tests
found two holes, both closed: a rebuilt object was not held to its entry's Location, and a
Group missing its last object on a track joins to another payload, which the text now states.

Corpora: wai/moqt-conformance/{moq-file,tempo,v3c}/ and
wai/live-conformance/carousel-moq-fec/, with moq_file_verify.py, tempo_verify.py,
v3c_verify.py and carousel_moq_fec_verify.py. The registry lists wai_tempo and wai_v3c
(export regenerated). wai/ietf/catalog_check.py holds them, fec and the packaging values
fec-repair, fec-source and v3c in three more writers' catalogs, with 31 more planted faults;
draft-charlot-moq-wai-packaging-01 is unchanged. joule-code-conformance.yml and
joule-moq-room.yml run the second implementations and trigger on the corpora and texts their
tests read; wai conform --independent runs four more verifiers.

Not done: no relay rewrites HopTime or reads the Sync Support parameter; nothing sends
stamped objects, FEC tracks or V3C tracks on a session; the drafts' MMTP packaging, FEC on
multicast paths and Reed-Solomon are not mapped.

Co-Authored-By: Claude <[email protected]>
2026-11-10 18:34:23 -05:00

584 lines
27 KiB
Python

#!/usr/bin/env python3
"""carousel_moq_fec_verify.py: a second implementation of the MoQ FEC catalog binding
(extensions/carousel.md §8.3) and of the multicast endpoint reading (bearer-receipts §9.7).
carousel_moq_fec_verify.py [CORPUS_DIR]
Written from those sections and from draft-ramadan-moq-fec-catalog-00, -fec-core-00 and
-fec-block-codes-00; it shares no code with wai-rs. For live-conformance/carousel-moq-fec/ it
binds every group's entries itself and requires the instances, the catalog tracks, each
object's SS_ID and block, its Protected Object Unit, its symbol-track objects and the header of
every repair object; reads every catalog back and compares it with the schedule; rebuilds every
object from its symbol track and holds it to its entry's content hash; gives every entry that
does not bind, every catalog and every endpoint its outcome; plants a symbol of another object
in the ways that need no decoding; and changes cases and requires each change to be caught.
It does not implement RFC 6330: the values of the repair symbols are checked by
wai-rs's tests/carousel_moq_fec.rs alone, against an independent implementation of that RFC.
K' is read from carousel.md Appendix A, and the content hash is claim-conformance's BLAKE3
(_blake3.py, written from its specification).
Exit 0 when everything holds, 1 with one line a finding otherwise. Standard library only.
"""
import copy
import ipaddress
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
from _blake3 import blake3 # noqa: E402 (written from its specification; no package)
K_MAX, MAX_ESI, MAX_LAYERS = 56403, (1 << 24) - 1, 256
DOMAIN_OBJECT = b"moq-jwp:object\x01"
GEOMETRY = ("scheme", "blockProfile", "mmtpMode", "timescale", "symbolSize", "interleaveDepthMs", "reorderToleranceMs", "sourceSymbols", "fec")
class Refuse(Exception):
pass
def k_primes():
with open(os.path.join(HERE, "..", "extensions", "carousel.md"), encoding="utf-8") as fh:
text = fh.read()
a, b = text.index("<!-- k-prime:begin -->"), text.index("<!-- k-prime:end -->")
return [int(x) for x in re.findall(r"[0-9]+", text[a + len("<!-- k-prime:begin -->"):b])]
K_PRIME = k_primes()
def k_prime(k):
return next((v for v in K_PRIME if v >= k), None)
def ceil_div(a, b):
return -(-a // b)
def varint_bytes(v):
for n in range(1, 9):
if v < 1 << (7 * n):
b = v.to_bytes(n, "big")
return bytes([b[0] | (0xFF << (9 - n)) & 0xFF if n > 1 else b[0]]) + b[1:]
return b"\xff" + v.to_bytes(8, "big")
def read_varint(b, at):
ones = 0
while ones < 8 and b[at] & (0x80 >> ones):
ones += 1
n = 9 if ones == 8 else ones + 1
if at + n > len(b):
raise IndexError
if n == 9:
return int.from_bytes(b[at + 1:at + 9], "big"), at + 9
return int.from_bytes(bytes([b[at] & (0xFF >> n)]) + b[at + 1:at + n], "big"), at + n
def immutable_properties(instance, ss_id):
"""fec-core §5.6: the FEC Source Info Property alone, type 0x11."""
value = (bytes([instance]) if instance else b"") + ss_id.to_bytes(4, "big")
return varint_bytes(0x11) + varint_bytes(len(value)) + value
def source_info(immutable, instance):
"""The SS_ID the properties give the object in `instance`, or None."""
at, ty, found = 0, 0, None
try:
while at < len(immutable):
d, at = read_varint(immutable, at)
ty += d
if ty % 2 == 0:
_, at = read_varint(immutable, at)
continue
n, at = read_varint(immutable, at)
if n > 0xFFFF or at + n > len(immutable):
return None
v, at = immutable[at:at + n], at + n
if ty == 0x0B or (ty == 0x11 and found is not None):
return None
if ty == 0x11:
found = v
except IndexError:
return None
if found is None:
return None
if len(found) == 4:
return int.from_bytes(found, "big") if instance == 0 else None
entries = [found[i:i + 5] for i in range(0, len(found), 5)]
if not found or len(found) % 5 or (len(entries) == 1 and entries[0][0] == 0) or any(a[0] >= b[0] for a, b in zip(entries, entries[1:])):
return None
for e in entries:
if e[0] == instance:
return int.from_bytes(e[1:], "big")
return None
def unit(group, obj, immutable, payload):
"""fec-core §5.2."""
return group.to_bytes(8, "big") + obj.to_bytes(8, "big") + len(immutable).to_bytes(2, "big") + immutable + len(payload).to_bytes(4, "big") + payload
def symbols(u, t):
return [u[i:i + t].ljust(t, b"\0") for i in range(0, len(u), t)]
def geometry(e, instance):
"""§8.3: (T, s, layers) for an entry whose object goes to `instance`."""
o = e["oti"]
if e["fec"] != "raptorq" or o["source_blocks"] != 1 or o["sub_blocks"] != 1:
raise Refuse("binding_no_form")
f, t = o["transfer_length"], o["symbol_size"]
if t % 8 or o["alignment"] != 8:
raise Refuse("binding_symbol_size")
k = ceil_div(f, t)
s = ceil_div(22 + (6 if instance == 0 else 7) + f, t)
if s > K_MAX or k_prime(s) != k_prime(k):
raise Refuse("binding_block_differs")
m = k + e["repair"]
if m - s < 1:
raise Refuse("binding_no_repair")
if e["esi"] == "repeat":
layers = [m - s]
else:
if e["count"] > MAX_LAYERS:
raise Refuse("binding_no_form")
layers = [m - s] + [m] * (e["count"] - 1)
return t, s, layers
def bind(entries, first_ss_id):
"""§8.3 Instances and layers: (instances, objects)."""
instances, nxt, objects = [], [], []
for e in entries:
g, at = None, None
try:
zero = geometry(e, 0)
if not instances or (instances[0]["symbol_size"], instances[0]["layers"]) == (zero[0], zero[2]):
g, at = zero, 0
except Refuse:
if not instances:
raise
if g is None:
g = geometry(e, 1)
at = next((i for i in range(1, len(instances)) if (instances[i]["symbol_size"], instances[i]["layers"]) == (g[0], g[2])), len(instances))
t, s, layers = g
if at == len(instances):
if at > 255:
raise Refuse("binding_too_many_instances")
instances.append({"id": at, "symbol_size": t, "source_symbols": 0, "layers": layers, "span_ms": 0})
nxt.append(first_ss_id)
i = instances[at]
i["source_symbols"], i["span_ms"] = max(i["source_symbols"], s), max(i["span_ms"], e["duration_ms"])
if i["source_symbols"] + sum(i["layers"]) - 1 > MAX_ESI:
raise Refuse("binding_no_form")
objects.append({"leaf_index": e["leaf_index"], "instance": at, "ss_id": nxt[at] % (1 << 32), "block_group": nxt[at], "symbols": s})
nxt[at] += s
if not objects:
raise Refuse("binding_no_form")
return instances, objects
def catalog_tracks(instances, track):
def rname(i, layer):
mid = "" if i == 0 else "/i%d" % i
return track + mid + "/repair" + ("" if layer == 0 else "/%d" % layer)
def sname(i):
return track + ("" if i == 0 else "/i%d" % i) + "/symbols"
fec, tracks = [], []
for i in instances:
fec.append({"instanceId": i["id"], "scheme": "raptorq", "blockProfile": "moq-object-v1", "sourceSymbols": i["source_symbols"],
"symbolSize": i["symbol_size"], "repairSymbols": i["layers"][0], "interleaveDepthMs": i["span_ms"],
"repairTrack": rname(i["id"], 0), "symbolTrack": sname(i["id"])})
tracks.append({"name": sname(i["id"]), "packaging": "fec-source", "isLive": True, "source": track, "instanceId": i["id"], "priority": 128})
for layer, p in enumerate(i["layers"]):
tracks.append({"name": rname(i["id"], layer), "packaging": "fec-repair", "isLive": True, "source": track,
"instanceId": i["id"], "repairLayer": layer, "repairSymbols": p, "priority": min(240 + layer, 255)})
return fec, tracks
def integer(m, k, lo, hi):
if k not in m:
return None
v = m[k]
if isinstance(v, bool) or not isinstance(v, int) or not lo <= v <= hi:
raise Refuse("binding_catalog_malformed")
return v
def text(m, k):
if k not in m:
return None
if not isinstance(m[k], str):
raise Refuse("binding_catalog_malformed")
return m[k]
def read_catalog(tracks, track):
"""§8.3 And back: the instances a catalog declares for `track`, failing closed."""
bad = Refuse("binding_catalog_malformed")
if any(not isinstance(t, dict) or "enhancementRepair" in t or any(isinstance(i, dict) and "enhancementRepair" in i for i in (t.get("fec") if isinstance(t.get("fec"), list) else [])) for t in tracks):
raise bad
def entry(name):
got = [t for t in tracks if t.get("name") == name]
if len(got) != 1:
raise bad
return got[0]
source = entry(track)
fec = source.get("fec")
if not isinstance(fec, list) or not fec:
raise bad
out, named, served = [], [], []
for f in fec:
if not isinstance(f, dict):
raise bad
iid = integer(f, "instanceId", 0, 255) or 0
if any(i["id"] == iid for i in out):
raise bad
if (text(f, "scheme"), text(f, "blockProfile")) != ("raptorq", "moq-object-v1"):
raise Refuse("binding_no_form")
k, t, p0 = integer(f, "sourceSymbols", 1, K_MAX), integer(f, "symbolSize", 8, 65535), integer(f, "repairSymbols", 1, MAX_ESI)
if k is None or t is None or t % 8 or p0 is None:
raise bad
span = integer(f, "interleaveDepthMs", 1, (1 << 64) - 1) or 0
if "reorderToleranceMs" in f and (isinstance(f["reorderToleranceMs"], bool) or not isinstance(f["reorderToleranceMs"], (int, float)) or f["reorderToleranceMs"] < 0 or f["reorderToleranceMs"] != f["reorderToleranceMs"]):
raise bad
if "label" in f and not isinstance(f["label"], str):
raise bad
layer0, sym = text(f, "repairTrack"), text(f, "symbolTrack")
if layer0 is None:
raise bad
for n in (layer0, sym):
if n is not None:
if n in named:
raise bad
named.append(n)
layers = {}
for tr in tracks:
name = text(tr, "name")
if name is None:
raise bad
is0 = name == layer0
if text(tr, "packaging") != "fec-repair":
if is0:
raise bad
continue
src = text(tr, "source")
if src is None:
serves = is0
else:
tid = integer(tr, "instanceId", 0, 255)
serves = src == track and (tid if tid is not None else (iid if is0 else 0)) == iid
if not serves:
if is0:
raise bad
continue
if any(g in tr for g in GEOMETRY):
raise bad
layer = integer(tr, "repairLayer", 0, MAX_ESI)
if layer is None:
if not is0:
raise bad
layer = 0
if (layer == 0) != is0:
raise bad
p = integer(tr, "repairSymbols", 1, MAX_ESI)
if p is None:
if layer != 0:
raise bad
p = p0
elif layer == 0 and p != p0:
raise bad
prio = integer(tr, "priority", 192, 255)
if layer in layers:
raise bad
layers[layer] = (name, p, 240 if prio is None else prio)
order = sorted(layers)
if not order or order != list(range(len(order))) or any(layers[a][2] > layers[b][2] for a, b in zip(order, order[1:])):
raise bad
if sym is not None:
st = entry(sym)
if text(st, "packaging") != "fec-source" or text(st, "source") not in (None, track) or integer(st, "instanceId", 0, 255) not in (None, iid) \
or "repairLayer" in st or "repairSymbols" in st or any(g in st for g in GEOMETRY):
raise bad
integer(st, "priority", 0, 255)
served.append(sym)
inst = {"id": iid, "symbol_size": t, "source_symbols": k, "layers": [layers[x][1] for x in order], "span_ms": span}
if k + sum(inst["layers"]) - 1 > MAX_ESI:
raise bad
served += [layers[x][0] for x in order]
out.append(inst)
for tr in tracks:
if text(tr, "packaging") in ("fec-repair", "fec-source") and text(tr, "source") in (None, track) and text(tr, "name") not in served:
raise bad
return sorted(out, key=lambda i: i["id"])
def same_instances(bound, read):
return len(bound) == len(read) and all(
a["id"] == b["id"] and a["symbol_size"] == b["symbol_size"] and a["layers"] == b["layers"] and a["source_symbols"] <= b["source_symbols"] and (b["span_ms"] == 0 or a["span_ms"] <= b["span_ms"])
for a, b in zip(bound, read))
def rebuild(block, inst, ss_start):
"""fec-core §9.2: the objects of a block's source symbols."""
bad = Refuse("binding_unit_malformed")
t = inst["symbol_size"]
if not block or len(block) % t:
raise bad
out, at = [], 0
while at < len(block):
if at + 18 > len(block):
raise bad
group, obj, il = int.from_bytes(block[at:at + 8], "big"), int.from_bytes(block[at + 8:at + 16], "big"), int.from_bytes(block[at + 16:at + 18], "big")
if at + 22 + il > len(block):
raise bad
immutable = block[at + 18:at + 18 + il]
pl = int.from_bytes(block[at + 18 + il:at + 22 + il], "big")
if at + 22 + il + pl > len(block):
raise bad
payload = block[at + 22 + il:at + 22 + il + pl]
nxt = at + ceil_div(22 + il + pl, t) * t
if nxt > len(block) or any(block[at + 22 + il + pl:nxt]):
raise bad
if source_info(immutable, inst["id"]) != (ss_start + at // t) % (1 << 32) or any((o[0], o[1]) == (group, obj) for o in out):
raise bad
out.append((group, obj, immutable, payload))
at = nxt
return out
def check_object(o, group, e):
if (o[0], o[1]) != (group, e["leaf_index"]):
raise Refuse("binding_unit_malformed")
if len(o[3]) != e["oti"]["transfer_length"] or blake3(DOMAIN_OBJECT + o[3]).hex() != e["content_hash"]:
raise Refuse("binding_content_mismatch")
def read_repair(b, t):
if len(b) != 13 + t:
return None
return {"ss_start": int.from_bytes(b[:4], "big"), "rsb": int.from_bytes(b[4:7], "big"), "rs_id": int.from_bytes(b[7:10], "big"), "ssb": int.from_bytes(b[10:13], "big"), "symbol": b[13:]}
def accept_repair(inst, block, layer, group, obj, payload):
"""§8.3 Symbols: whether a repair object fits the block (its Group and, once known, SSB_length)."""
r = read_repair(payload, inst["symbol_size"])
return not (r is None or group != block["group"] or group % (1 << 32) != r["ss_start"] or r["ssb"] == 0 or r["ssb"] > inst["source_symbols"]
or (block["ssb"] is not None and block["ssb"] != r["ssb"]) or r["rsb"] != sum(inst["layers"]) or layer >= len(inst["layers"])
or obj >= inst["layers"][layer] or r["rs_id"] != sum(inst["layers"][:layer]) + obj)
def accept_symbol(inst, block, group, obj, payload):
limit = block["ssb"] if block["ssb"] is not None else inst["source_symbols"]
return group == block["group"] and len(payload) == inst["symbol_size"] and obj < limit
def in_mode(group, ssm):
if group.version == 4:
return group.is_multicast and (group.packed[0] == 232) == ssm
p = group.packed
return p[0] == 0xFF and (p[1] >> 4 == 3 and p[2] == 0 and p[3] == 0) == ssm
def read_endpoint(e, tracks, auth):
"""bearer-receipts §9.7, in its order: "stands" or the label the endpoint is ignored with."""
if not isinstance(e, dict) or not isinstance(e.get("groupAddress"), str) or not isinstance(e.get("tracks"), list) or not e["tracks"] \
or any(not isinstance(t, dict) for t in e["tracks"]) or any(k in e and not isinstance(e[k], str) for k in ("sourceAddress", "protocol")):
return "endpoint_malformed"
bw = e.get("bandwidth")
if "bandwidth" in e and (isinstance(bw, bool) or not isinstance(bw, int) or not 0 <= bw < 1 << 53):
return "endpoint_malformed"
try:
group = ipaddress.ip_address(e["groupAddress"])
source = ipaddress.ip_address(e["sourceAddress"]) if "sourceAddress" in e else None
except ValueError:
return "endpoint_address"
if (source is not None and source.version != group.version) or not in_mode(group, source is not None):
return "endpoint_address"
if "protocol" in e and e["protocol"] != ("ssm" if source is not None else "asm"):
return "endpoint_mode"
port = e.get("port")
if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535:
return "endpoint_port"
for t in e["tracks"]:
pid = t.get("packetId")
if not isinstance(t.get("name"), str) or isinstance(pid, bool) or not isinstance(pid, int) or not 1 <= pid <= 65535:
return "endpoint_packet_id"
def find(name):
got = [t for t in tracks if isinstance(t, dict) and t.get("name") == name]
return got[0] if len(got) == 1 else None
for t in e["tracks"]:
tr = find(t["name"])
if tr is None or "namespace" in tr:
return "endpoint_track"
if tr.get("packaging") == "mmtp":
continue
src = find(tr.get("source")) if tr.get("packaging") == "fec-repair" and isinstance(tr.get("source"), str) else None
if src is None or src.get("packaging") != "mmtp":
return "endpoint_track"
if ("auth" in e) != auth or ("auth" in e and "bandwidth" not in e):
return "endpoint_auth"
return "stands"
def read_endpoints(catalog):
if "multicast" not in catalog:
return []
mc = catalog["multicast"]
eps = mc.get("endpoints") if isinstance(mc, dict) else None
if not isinstance(eps, list) or not eps:
return "endpoints_malformed"
tracks = catalog.get("tracks") if isinstance(catalog.get("tracks"), list) else []
return [read_endpoint(e, tracks, "auth" in mc) for e in eps]
def outcome(f, *a):
try:
return f(*a)
except Refuse as r:
return str(r)
def check(doc):
fails = []
for g in doc["groups"]:
what = g["what"]
got = outcome(bind, g["entries"], g["first_ss_id"])
if isinstance(got, str):
fails.append("%s: does not bind: %s" % (what, got))
continue
instances, objects = got
fec, tracks = catalog_tracks(instances, g["track"])
if (fec, tracks) != (g["fec"], g["tracks"]):
fails.append("%s: the catalog tracks are not the ones stated" % what)
read = outcome(read_catalog, [{"name": g["track"], "packaging": "wai", "isLive": True, "fec": g["fec"]}] + g["tracks"], g["track"])
if read != instances or not same_instances(instances, read):
fails.append("%s: the catalog does not read back to the schedule's instances: %s" % (what, read))
for o, w, e, env in zip(objects, g["objects"], g["entries"], g["envelopes"]):
inst = instances[o["instance"]]
t = inst["symbol_size"]
at = "%s, object %d" % (what, o["leaf_index"])
if any(o[k] != w[k] for k in o):
fails.append("%s: its instance or block is not the one stated" % at)
continue
payload = bytes.fromhex(env)
imm = immutable_properties(o["instance"], o["ss_id"])
u = unit(g["group_id"], o["leaf_index"], imm, payload)
src = symbols(u, t)
if imm.hex() != w["immutable"] or u.hex() != w["unit"] or [s.hex() for s in src] != w["symbol_track"] or len(src) != o["symbols"]:
fails.append("%s: its unit or its source symbols are not the ones stated" % at)
# Everything §6 derives from the entry holds of the block: the count a pass, and K'.
k = ceil_div(e["oti"]["transfer_length"], t)
if o["symbols"] + inst["layers"][0] != k + e["repair"] or k_prime(o["symbols"]) != k_prime(k):
fails.append("%s: a pass does not send the schedule's symbols" % at)
# The repair objects' headers, Locations and layers.
block = {"group": o["block_group"], "ssb": None}
total = sum(inst["layers"])
if len(w["repair"]) != total:
fails.append("%s: %d repair objects for %d repair symbols" % (at, len(w["repair"]), total))
for rs, r in enumerate(w["repair"]):
layer = max(x for x in range(len(inst["layers"])) if sum(inst["layers"][:x]) <= rs)
hdr = read_repair(bytes.fromhex(r["hex"]), t)
if (r["layer"], r["group"], r["object"]) != (layer, o["block_group"], rs - sum(inst["layers"][:layer])) or hdr is None \
or (hdr["ss_start"], hdr["rsb"], hdr["rs_id"], hdr["ssb"]) != (o["ss_id"], total, rs, o["symbols"]) \
or not accept_repair(inst, block, r["layer"], r["group"], r["object"], bytes.fromhex(r["hex"])):
fails.append("%s: repair object %d is not where §8.3 puts it" % (at, rs))
block["ssb"] = o["symbols"]
# Rebuilt from its symbol track, it is the object its entry signs.
held = b"".join(bytes.fromhex(s) for s in w["symbol_track"])
got = outcome(rebuild, held, inst, o["ss_id"])
if isinstance(got, str) or len(got) != 1 or got[0][3] != payload or outcome(check_object, got[0], g["group_id"], e) is not None:
fails.append("%s: it does not rebuild to the object scheduled" % at)
# And back: the entry's code members from the instance and the block.
if (inst["symbol_size"], o["symbols"] + inst["layers"][0] - k) != (e["oti"]["symbol_size"], e["repair"]):
fails.append("%s: the entry's members do not come back" % at)
# A symbol of another object, in the ways that need no decoding.
first = [i for i, o in enumerate(objects) if o["instance"] == objects[0]["instance"]]
if len(first) >= 2:
a, b = objects[first[0]], objects[first[1]]
wa, wb = g["objects"][first[0]], g["objects"][first[1]]
inst = instances[a["instance"]]
block = {"group": a["block_group"], "ssb": a["symbols"]}
other = bytes.fromhex(wb["repair"][0]["hex"])
forged = a["ss_id"].to_bytes(4, "big") + other[4:]
if accept_symbol(inst, block, b["block_group"], 0, bytes.fromhex(wb["symbol_track"][0])) or accept_repair(inst, block, 0, b["block_group"], 0, other) \
or accept_repair(inst, block, 0, a["block_group"], 0, other) or accept_repair(inst, block, 0, a["block_group"], 0, forged) \
or accept_symbol(inst, block, a["block_group"], a["symbols"], bytes.fromhex(wa["symbol_track"][0])):
fails.append("%s: a symbol of another object was placed" % what)
for esi, want in ((0, "binding_unit_malformed"), (min(6, a["symbols"] - 2), "binding_content_mismatch")):
held = [bytes.fromhex(s) for s in wa["symbol_track"]]
held[esi] = bytes.fromhex(wb["symbol_track"][esi])
got = outcome(rebuild, b"".join(held), inst, a["ss_id"])
if not isinstance(got, str):
got = outcome(check_object, got[0], g["group_id"], g["entries"][first[0]])
if got != want:
fails.append("%s: the other object's symbol %d in place gave %s, not %s" % (what, esi, got, want))
for c in doc["unbound"]:
got = outcome(bind, [c["entry"]], 0)
if got != c["reason"]:
fails.append("unbound %r: %s, stated %s" % (c["what"], got if isinstance(got, str) else "bound", c["reason"]))
for c in doc["catalogs"]:
got = outcome(read_catalog, c["tracks"], c["tracks"][0]["name"])
got = got if isinstance(got, str) else "accept"
if got != c["outcome"]:
fails.append("catalog %r: %s, stated %s" % (c["what"], got, c["outcome"]))
for c in doc["endpoints"]:
got = read_endpoints(c["catalog"])
if got != c["outcome"]:
fails.append("endpoints %r: %s, stated %s" % (c["what"], got, c["outcome"]))
if len(doc["groups"]) < 3 or len(doc["unbound"]) < 9 or len(doc["catalogs"]) < 30 or len(doc["endpoints"]) < 6:
fails.append("the corpus is smaller than it was written to be")
return fails
def main(argv):
d = argv[1] if len(argv) > 1 else os.path.join(HERE, "carousel-moq-fec")
with open(os.path.join(d, "vectors.json"), encoding="utf-8") as fh:
doc = json.load(fh)
if K_PRIME[0] != 10 or K_PRIME[-1] != K_MAX or len(K_PRIME) != 477:
print("FAIL carousel.md Appendix A is not the table of K'")
return 1
fails = check(doc)
def flip(h):
return ("00" if h[:2] != "00" else "01") + h[2:]
edits = [
("an envelope octet", lambda x: x["groups"][0]["envelopes"].__setitem__(0, flip(x["groups"][0]["envelopes"][0]))),
("a symbol-track object", lambda x: x["groups"][0]["objects"][1]["symbol_track"].__setitem__(3, flip(x["groups"][0]["objects"][1]["symbol_track"][3]))),
("an SS_ID", lambda x: x["groups"][0]["objects"][1].__setitem__("ss_id", x["groups"][0]["objects"][1]["ss_id"] + 1)),
("a repair object's RS_ID", lambda x: x["groups"][0]["objects"][0]["repair"][2].__setitem__("hex", x["groups"][0]["objects"][0]["repair"][3]["hex"])),
("a repair object's layer", lambda x: x["groups"][1]["objects"][0]["repair"][8].__setitem__("layer", 0)),
("an entry's repair", lambda x: x["groups"][0]["entries"][0].__setitem__("repair", 10)),
("an entry's content hash", lambda x: x["groups"][2]["entries"][0].__setitem__("content_hash", "00" * 32)),
("the catalog's repairSymbols", lambda x: x["groups"][0]["fec"][0].__setitem__("repairSymbols", 8)),
("a repair track's layer", lambda x: x["groups"][1]["tracks"][2].__setitem__("repairLayer", 2)),
("an unbound entry's reason", lambda x: x["unbound"][0].__setitem__("reason", "binding_symbol_size")),
("an unbound entry made good", lambda x: x["unbound"][4].__setitem__("entry", x["groups"][0]["entries"][0])),
("a catalog's outcome", lambda x: x["catalogs"][5].__setitem__("outcome", "accept")),
("a good catalog's outcome", lambda x: x["catalogs"][0].__setitem__("outcome", "binding_catalog_malformed")),
("an endpoint's outcome", lambda x: x["endpoints"][0]["outcome"].__setitem__(0, "endpoint_port")),
("an ignored endpoint stated to stand", lambda x: x["endpoints"][0]["outcome"].__setitem__(11, "stands")),
]
for what, e in edits:
changed = copy.deepcopy(doc)
e(changed)
if not check(changed):
fails.append("a change went unnoticed: %s" % what)
for f in fails:
print("FAIL " + f)
n = sum(len(g["objects"]) for g in doc["groups"])
print("carousel-moq-fec: %d objects of %d groups, %d unbound, %d catalogs, %d endpoint lists, %d edits; %d failure(s)" % (n, len(doc["groups"]), len(doc["unbound"]), len(doc["catalogs"]), len(doc["endpoints"]), len(edits), len(fails)))
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))