open-standards/wai/live-conformance/carousel_verify.py
Transaction Science 869fd65efc wai: a schedule names Raptor (RFC 5053), the code MBMS download uses
Carousel §4.1, §5.5: an entry's fec may be raptor (FEC Encoding ID 1), with RFC
5053's F, T, Z, N and Al, held to its fields and to its block sizes, 4 to 8,192
source symbols, with ESIs in 16 bits. 3GPP TS 26.346 §7.2.2 requires a receiver
of MBMS download to support the code.

Carousel §5.3: RFC 5053 states no bound on a decoder's failure. An independent
implementation failed on 14 to 31 blocks in 100 at K + 2, so a Raptor block is
held at K and a margin set by measurement, 8, 16 or 28 at overhead 0, 1 or 2,
and failure_one_in is null. tests/carousel.rs decodes each Raptor object a case
holds available with that implementation, a new development dependency, holds a
fixed sample of it to the measurement, and keeps the measurement as an ignored
test.

no-code stays the default, and carousel §11 keeps the IETF's disclosures on RFC
5053 for implementers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:09:38 -05:00

640 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Independent verifier of the carousel corpus (extensions/carousel.md).
Written from the extension's text. It shares no code with wai-rs, which generates the corpus,
and never runs it. It takes from the second implementations of the texts this extension builds
on what they define: group and object receipts from the JWP corpus's
(`../jwp-conformance/jwp_verify.py`, jwp-receipts §2.2, §3), and a claim's steps 1 to 5 from the
claim corpus's (`../claim-conformance/claim_verify.py`, jwp-receipts §3.1), whose step 4 holds
the schedule class's rules with §5.1 and §5.2's division of an object into source blocks.
A key log's reading comes from the signer corpus's (`../signer-conformance/signer_verify.py`,
jwp-receipts §7.2). Everything else this extension adds is written here:
- §4.3: a schedule against the group, its object receipts and their envelopes;
- §4.5: when a later schedule extends an earlier one;
- §5.3: what a receiver needs of a block, K′ read from Appendix A of the extension's text;
- §6: the declarations and schedules that stand, each entry's availability within a loss bound,
and statements offered;
- §7: the passes a receiver listens to;
- §8.1 and §8.2: a schedule's interval, and whether what a receiver acquired suffices.
For every case it requires the outcome the corpus states, field for field; that every reason
of §9's table appears in the corpus; then it mutates copies of the corpus, one defect each, and
requires every copy to fail; and it drops each of its own checks in turn and requires the corpus
to catch the weakened verifier.
python3 carousel_verify.py <live-conformance/carousel>
Exit status 0 only if every case verifies, every mutated copy fails and every weakened verifier
is caught. Python 3.8 or later; the `cryptography` package, which the claim verifier imports.
"""
import copy
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
for d in ("claim-conformance", "jwp-conformance", "signer-conformance"):
sys.path.insert(0, os.path.join(HERE, "..", d))
import claim_verify as CV # noqa: E402
import jwp_verify as JV # noqa: E402
import signer_verify as SG # noqa: E402
EXTENSION = os.path.join(HERE, "..", "extensions", "carousel.md")
# A weakened run drops one of these checks (see `WEAKENINGS`); None checks everything.
DROPPED = None
def drops(name):
return DROPPED == name
def extension():
return open(EXTENSION, encoding="utf-8").read()
def reasons_table():
"""§9's codes, in order."""
text = extension()
at = text.index("\n## 9. Reasons")
end = text.find("\n## ", at + 1)
rows = text[at:end if end >= 0 else len(text)].splitlines()
return [r.split("`")[1] for r in rows if r.startswith("| `")]
def k_primes():
"""Appendix A: the column K′ of RFC 6330 Table 2."""
text = extension()
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()
RAPTOR_MARGIN = (8, 16, 28) # §5.3: the symbols beyond K a Raptor block is held at, at each overhead
def k_prime(k):
"""§5.3: the least K′ at least k."""
return next(v for v in K_PRIME if v >= k)
def need(ok, reason):
if not ok:
raise CV.Reject(6, reason)
def included(o, g):
try:
JV.inclusion(o, g)
return True
except JV.Refuse:
return False
def accepted(o, g, envelope):
try:
JV.accept(o, g, envelope)
return True
except JV.Refuse:
return False
# ------------------------------------------------------------------ §4.3: step 6
def check_transmission(c, g):
"""bearer-receipts §4.3."""
need(JV.link(g).hex() == c["subject"], "transmission_subject_mismatch")
need(g["signer_pubkey"] == c["signer_pubkey"], "transmission_signer_mismatch")
def check_schedule(c, g, objects, node=False):
"""§4.3 against a group that passed jwp-receipts §3 steps 1 and 2, and the object receipts
held under it with their envelopes where held: [(object, envelope or None)]. A node's schedule
of its own instance (cache-authority §5.6) is checked in every step but step 2."""
start = JV.seconds(g["group_start"])
need(start is not None, "context_invalid")
entries = c["body"]["objects"]
need(drops("subject") or JV.link(g).hex() == c["subject"], "schedule_subject_mismatch")
need(node or drops("signer") or g["signer_pubkey"] == c["signer_pubkey"], "schedule_signer_mismatch")
need(drops("range") or all(e["leaf_index"] < g["leaf_count"] for e in entries), "schedule_leaf_out_of_range")
need(drops("before") or all(e["first_ms"] // 1000 >= start for e in entries), "schedule_before_group")
for e in entries:
for o, envelope in objects:
if o["object_id"] != e["leaf_index"] or not included(o, g):
continue
need(drops("content") or o["content_hash"].hex() == e["content_hash"], "schedule_content_mismatch")
if envelope is not None and accepted(o, g, envelope):
need(drops("length") or len(envelope) == e["oti"]["transfer_length"], "schedule_length_mismatch")
# ------------------------------------------------------------------ §4.5
def entry_extends(b, a):
"""§4.5: entry b's passes begin with entry a's: its members are a's but `count`, at least a's,
and `period_ms`, which may differ only where a has one pass."""
if b["count"] < a["count"] and not drops("extension_count"):
return False
period_read = a["count"] >= 2 or drops("extension_one_pass")
if period_read and b["period_ms"] != a["period_ms"] and not drops("extension_period"):
return False
return drops("extension_members") or all(b[k] == v for k, v in a.items() if k not in ("count", "period_ms"))
def extends(b, a):
"""§4.5: schedule b extends schedule a of its instance: for every entry of a, an entry of that
object that extends it. It may have entries for objects a has none for."""
by_leaf = {e["leaf_index"]: e for e in b["objects"]}
def covered(e):
later = by_leaf.get(e["leaf_index"])
return drops("extension_drops") if later is None else entry_extends(later, e)
return all(covered(e) for e in a["objects"])
def monotone(bodies):
"""§6.2 step 4: the schedules of one instance, in chain order: each extends the one before."""
if drops("extension_chain"):
return all(extends(b, bodies[0]) for b in bodies[1:])
return all(extends(b, a) for a, b in zip(bodies, bodies[1:]))
# ------------------------------------------------------------------ §5.3, §6.3
def needed(e, k, h):
"""§5.3: every source symbol for no-code, K′ + h for RaptorQ, K and its margin for Raptor."""
if e["fec"] == "raptor" and not drops("raptor_k_prime"):
return k + (h if drops("raptor_margin") else RAPTOR_MARGIN[h])
return k if e["fec"] == "no-code" else k_prime(k) + h
def missed(loss, m):
"""§2: ⌊L · m / 1,000,000⌋."""
return loss * m // 1_000_000
def pass_end(e, j):
return e["first_ms"] + (j - 1) * e["period_ms"] + e["duration_ms"]
def block_pass(e, k, loss, h):
"""§6.3: the first pass after which a receiver within the bound holds a block of k."""
m = k + e["repair"]
x = missed(loss, m)
need_ = needed(e, k, h)
if e["fec"] != "no-code" and e["esi"] == "advance":
j = CV.ceil_div(need_, m - x)
elif e["fec"] != "no-code":
j = 1 if m - x >= need_ else None
else:
j = 1 if x == 0 or drops("no_code_loss") else None
return j if j is not None and j <= e["count"] else None
def availability(e, loss, h):
blocks = CV.schedule_blocks(e)
passes = [block_pass(e, k, loss, h) for k, _ in blocks]
j = None if None in passes else max(passes)
return {
"pass": j,
"available_ms": None if j is None else pass_end(e, j),
"bytes_sent": CV.schedule_bytes(e, blocks),
# §6.3: RFC 6330 §5.8's bound; RFC 5053 states none, and no-code recovers exactly.
"failure_one_in": [100, 10_000, 1_000_000][h] if e["fec"] == "raptorq" or (e["fec"] == "raptor" and drops("raptor_bound")) else None,
}
# ------------------------------------------------------------------ §6: the verdict
def bounds(loss, h):
if loss > 999_999:
return "loss_bound_invalid"
if h not in (0, 1, 2):
return "overhead_invalid"
return None
def read_group(wire):
"""§6.2 step 1's group, or the refusal the corpus states."""
try:
g = JV.read_group(wire)
JV.verify_group(g)
except JV.Refuse as r:
return None, {"outcome": "refuse", "reason": r.reason}
return g, None
def judge(g, objs, wires):
"""§6.2 steps 2 to 4 over a group and the object receipts held: the claims' labels, and the
claim that stands on each instance, by its `bearer_id`."""
# Step 2: each claim, §3.1 steps 1-5, then step 6.
held, claims, standing_claims = {}, [], []
for wire in wires:
try:
c = CV.verify_claim(wire)
except CV.Reject as r:
claims.append(f"reject step={r.step} reason={r.reason}")
standing_claims.append(None)
continue
previous = held.get(c["signer_pubkey"])
held[c["signer_pubkey"]] = c
try:
CV.check_chain(c, previous)
if c["class"] == "transmission":
check_transmission(c, g)
elif c["class"] == "schedule":
check_schedule(c, g, objs)
else:
raise CV.Reject(6, "context_mismatch")
except CV.Reject as r:
claims.append(f"reject step={r.step} reason={r.reason}")
standing_claims.append(None)
continue
claims.append("stands" if c["class"] == "schedule" else "declared")
standing_claims.append(c)
def by_instance(cls):
out = {}
for i, c in enumerate(standing_claims):
if c is not None and c["class"] == cls:
out.setdefault(c["body"]["bearer_id"], []).append(i)
return out
# Step 3: the declarations.
declared, contradicted = {}, set()
for bid, idx in by_instance("transmission").items():
bodies = [standing_claims[i]["body"] for i in idx]
if all(b == bodies[0] for b in bodies):
declared[bid] = bodies[0]["bearer"]
else:
contradicted.add(bid)
for i in idx:
claims[i] = "set-aside reason=bearer_contradicted"
# Step 4: the schedules, each instance's in the order of their signer's chain.
schedules = {}
for bid, idx in by_instance("schedule").items():
bodies = [standing_claims[i]["body"] for i in idx]
if not drops("schedule_contradiction") and not monotone(bodies):
aside = "schedule_contradicted"
elif bid in contradicted:
aside = "bearer_contradicted"
elif bid not in declared and not drops("undeclared"):
aside = "bearer_not_declared"
elif declared.get(bid) == "bidirectional" and not drops("bidirectional"):
aside = "schedule_on_bidirectional"
else:
aside = None
if aside:
for i in idx:
claims[i] = "set-aside reason=" + aside
else:
last = idx[0] if drops("extension_last") else idx[-1]
schedules[bid] = standing_claims[last]
for i in idx:
if standing_claims[i]["body"] != standing_claims[last]["body"]:
claims[i] = "extended"
return claims, schedules
def verdict(case):
bad = bounds(case["loss_ppm"], case["overhead"])
if bad:
return {"outcome": "refuse", "reason": bad}
g, refused = read_group(case["group"])
if refused:
return refused
# Step 1: the objects, the same on every bearer.
objs, overdicts = [], []
for o in case["objects"]:
try:
ob = JV.read_object(o["receipt"])
except JV.Refuse as r:
overdicts.append("reject reason=" + r.reason)
continue
env = None if o["envelope"] is None else bytes.fromhex(o["envelope"])
objs.append((ob, env))
try:
if env is None:
JV.inclusion(ob, g)
overdicts.append("inclusion-verified")
else:
JV.accept(ob, g, env)
overdicts.append("delivered-verified")
except JV.Refuse as r:
overdicts.append("reject reason=" + r.reason)
claims, schedules = judge(g, objs, case["claims"])
# Step 5.
loss, h = case["loss_ppm"], case["overhead"]
lines = []
for bid in sorted(schedules):
for e in schedules[bid]["body"]["objects"]:
lines.append({"bearer_id": bid, "leaf_index": e["leaf_index"], **availability(e, loss, h)})
# §6.4.
def held_by(bid, i, by):
line = next((x for x in lines if x["bearer_id"] == bid and x["leaf_index"] == i), None)
if line is None:
return "object_not_scheduled"
if line["available_ms"] is None:
return "availability_not_within_schedule"
if line["available_ms"] > by and not drops("deadline"):
return "availability_after_deadline"
return None
statement = None
s = case["statement"]
if s is not None:
if s["kind"] == "object":
r = held_by(s["bearer_id"], s["index"], s["by_ms"])
else:
r = next((r for r in (held_by(s["bearer_id"], i, s["by_ms"]) for i in range(g["leaf_count"])) if r), None)
statement = r or "holds"
return {"outcome": "verdict", "objects": overdicts, "claims": claims, "lines": lines, "statement": statement}
# ------------------------------------------------------------------ §7: a plan
def entry_of(v):
"""A plan's entry, read as a schedule's entry is at jwp-receipts §3.1 steps 2 and 4."""
body = {"bearer_id": "urn:example:plan", "objects": [v], "attests": "scheduled"}
CV.check_body("schedule", body)
CV.schedule_entry_rules(v)
return v
def plan(case):
e = entry_of(case["entry"])
loss, h, t, held = case["loss_ppm"], case["overhead"], case["at_ms"], case["held"]
bad = bounds(loss, h)
if bad:
return {"outcome": "refuse", "reason": bad}
ks = [k for k, c in CV.schedule_blocks(e) for _ in range(c)]
if len(held) != len(ks) or (e["fec"] == "no-code" and any(n > k for n, k in zip(held, ks))
and not drops("holding")):
return {"outcome": "refuse", "reason": "holding_invalid"}
n = list(held)
open_ = lambda: [b for b, k in enumerate(ks) if n[b] < needed(e, k, h)] # noqa: E731
first = 1 if t <= e["first_ms"] else (2 if e["count"] == 1 else CV.ceil_div(t - e["first_ms"], e["period_ms"]) + 1)
if drops("late_pass"):
first = 1 if t <= e["first_ms"] else (t - e["first_ms"]) // max(1, e["period_ms"]) + 1
if not open_():
return {"outcome": "plan", "passes": [], "complete_ms": t, "listen_ms": 0}
passes = []
for j in range(first, e["count"] + 1):
blocks = open_()
if not blocks:
break
gains = {}
for b in blocks:
m = ks[b] + e["repair"]
x = missed(loss, m)
if e["fec"] != "no-code" and e["esi"] == "advance":
gains[b] = m - x
else:
gains[b] = max(0, m - n[b] - (0 if drops("again") else x))
if not any(gains.values()):
break
for b, gain in gains.items():
n[b] += gain
passes.append(j)
complete = None if open_() else pass_end(e, passes[-1])
return {
"outcome": "plan",
"passes": [{"pass": j, "start_ms": e["first_ms"] + (j - 1) * e["period_ms"], "end_ms": pass_end(e, j)} for j in passes],
"complete_ms": complete,
"listen_ms": len(passes) * e["duration_ms"],
}
# ------------------------------------------------------------------ §8.1, §8.2: what rides in-band
def interval(body):
"""§8.1: the least `period_ms` of the entries of two passes or more, or, where every entry has
one pass, the least `duration_ms`."""
repeated = [e["period_ms"] for e in body["objects"] if e["count"] >= 2 or drops("interval_one_pass")]
if repeated:
return min(repeated)
return min(e["duration_ms"] for e in body["objects"])
def key_log_held(c, logs):
"""§8.2 check 3: a log acquired is held for the origin the claim's `signer_id` names
(jwp-receipts §7.2 rules 1 to 3) and lists the claim's key."""
named = SG.classify(c["signer_id"])
if named[0] != "origin":
return False
text = logs.get(named[1])
if text is None:
return False
try:
r = SG.parse_revision(text)
if not drops("inband_log_held"):
SG.check_held(r, named[1])
except SG.Stop:
return False
return drops("inband_log_lists") or any(e["key"] == c["signer_pubkey"] for e in r["keys"])
def inband(case):
"""§8.2: §6.2 over the group and the set's claims alone, then its three checks in order."""
g, refused = read_group(case["group"])
if refused:
return refused
claims, schedules = judge(g, [], case["claims"])
c = schedules.get(case["bearer_id"])
if any(label.startswith("reject step=5 ") for label in claims) and not drops("inband_chain"):
result = "inband_chain_incomplete"
elif c is None:
result = "inband_not_scheduled"
elif not drops("inband_log") and not key_log_held(c, case["logs"]):
result = "inband_key_log_missing"
else:
result = "suffices"
return {
"outcome": "inband",
"claims": claims,
"set": result,
"inband_ms": interval(c["body"]) if result == "suffices" else None,
}
# ------------------------------------------------------------------ the corpus
def strings(v, out):
if isinstance(v, str):
out.append(v)
elif isinstance(v, list):
for x in v:
strings(x, out)
elif isinstance(v, dict):
for x in v.values():
strings(x, out)
def claim_outcome(wire):
try:
CV.verify_claim(wire)
return "accept"
except CV.Reject as r:
return f"reject step={r.step} reason={r.reason}"
def check(doc, quiet=False):
"""The failures of a corpus document: each case's outcome, and every reason of §9."""
fails = []
for kind, run in (("verdicts", verdict), ("claims", lambda c: claim_outcome(c["claim"])), ("plans", plan), ("inband", inband)):
for c in doc[kind]:
try:
got = run(c)
except CV.Reject as r:
got = {"error": f"reject step={r.step} reason={r.reason}"}
except (KeyError, TypeError, ValueError, IndexError, AttributeError, StopIteration, ZeroDivisionError) as e:
got = {"error": repr(e)}
if got != c["outcome"]:
fails.append(c["name"])
if not quiet:
print(f"FAIL {c['name']}\n stated {json.dumps(c['outcome'])}\n got {json.dumps(got)}")
stated = []
for kind in ("verdicts", "claims", "plans", "inband"):
for c in doc[kind]:
strings(c["outcome"], stated)
for code in reasons_table():
if not any(s == code or s.endswith("reason=" + code) for s in stated):
fails.append("no case gives " + code)
return fails
def mutations(doc):
"""Copies of the corpus, one defect each, every one of which must fail."""
def case(d, name):
return next(c for k in ("verdicts", "claims", "plans", "inband") for c in d[k] if c["name"] == name)
def entry(c, i):
w = json.loads(c["claims"][i])
return w, w["body"]["objects"]
def m_pass(d):
case(d, "loss_30_percent")["outcome"]["lines"][0]["pass"] = 1
def m_bytes(d):
case(d, "lossless")["outcome"]["lines"][0]["bytes_sent"] += 64
def m_failure(d):
case(d, "overhead_1")["outcome"]["lines"][0]["failure_one_in"] = 100
def m_loss(d):
# 88%: object 0 gains 3 a pass, and needs 7.
case(d, "loss_85_percent_last_pass")["loss_ppm"] = 880_000
def m_repair(d):
c = case(d, "lossless")
w, es = entry(c, 1)
es[0]["repair"] = 7
c["claims"][1] = json.dumps(w, separators=(",", ":"), sort_keys=True)
def m_statement(d):
case(d, "object_after_deadline")["statement"]["by_ms"] += 1
def m_plan(d):
case(d, "too_late")["outcome"]["complete_ms"] = case(d, "too_late")["outcome"]["passes"][0]["end_ms"]
def m_held(d):
# 19 short of a block whose pass adds 18: two passes.
case(d, "holding_part")["held"][0] = 1
def m_accept(d):
case(d, "bytes_greatest")["outcome"] = "reject step=4 reason=schedule_passes_invalid"
def m_reason_gone(d):
d["verdicts"].remove(case(d, "schedule_on_bidirectional"))
def m_extended_label(d):
case(d, "extension_adds_passes")["outcome"]["claims"][1] = "stands"
def m_extended_pass(d):
# Under the first schedule's five passes the object is not within the schedule.
c = case(d, "extension_adds_passes")
c["claims"].pop()
def m_withdrawn(d):
# The same schedules in the order that extends: nothing is contradicted.
c = case(d, "extension_withdraws_passes")
c["claims"][1], c["claims"][2] = c["claims"][2], c["claims"][1]
def m_interval(d):
case(d, "suffices")["outcome"]["inband_ms"] = 20_000
def m_set(d):
case(d, "key_log_absent")["logs"] = case(d, "suffices")["logs"]
def m_raptor_fec(d):
# The same parameters under RaptorQ, which object 1 at 30% would reach.
c = case(d, "raptor_30_percent")
w = json.loads(c["claims"][1])
w["body"]["objects"][1]["fec"] = "raptorq"
c["claims"][1] = json.dumps(w, separators=(",", ":"), sort_keys=True)
def m_raptor_bound(d):
case(d, "raptor_lossless")["outcome"]["lines"][0]["failure_one_in"] = 1_000_000
def m_set_reason_gone(d):
d["inband"] = [c for c in d["inband"] if c["outcome"].get("set") != "inband_chain_incomplete"]
return [m_pass, m_bytes, m_failure, m_loss, m_repair, m_statement, m_plan, m_held, m_accept, m_reason_gone,
m_extended_label, m_extended_pass, m_withdrawn, m_interval, m_set, m_set_reason_gone, m_raptor_fec,
m_raptor_bound]
WEAKENINGS = ["subject", "signer", "range", "before", "content", "length", "no_code_loss", "schedule_contradiction",
"undeclared", "bidirectional", "deadline", "holding", "late_pass", "again", "extension_count",
"extension_one_pass", "extension_period", "extension_members", "extension_drops", "extension_chain",
"extension_last", "interval_one_pass", "inband_chain", "inband_log", "inband_log_held",
"inband_log_lists", "raptor_k_prime", "raptor_margin", "raptor_bound"]
def main(argv):
global DROPPED
if len(argv) != 2:
print(__doc__)
return 2
doc = json.load(open(os.path.join(argv[1], "vectors.json"), encoding="utf-8"))
extra = sorted(set(os.listdir(argv[1])) - {"vectors.json", "README.md"})
fails = check(doc)
if extra:
fails.append(f"files the corpus does not hold: {extra}")
if len(K_PRIME) != 477 or any(a >= b for a, b in zip(K_PRIME, K_PRIME[1:])) or K_PRIME[-1] != CV.K_PRIME_MAX:
fails.append("Appendix A is not 477 increasing values ending at K'max")
print(f"{len(doc['verdicts'])} verdict cases, {len(doc['claims'])} claim cases, {len(doc['plans'])} plans, "
f"{len(doc['inband'])} in-band sets: {len(fails)} failure(s)")
caught = 0
for m in mutations(doc):
d = copy.deepcopy(doc)
m(d)
if check(d, quiet=True):
caught += 1
else:
fails.append(f"mutation {m.__name__} was not caught")
print(f"{caught} of {len(mutations(doc))} mutated copies fail")
weak = 0
for w in WEAKENINGS:
DROPPED = w
if check(doc, quiet=True):
weak += 1
else:
fails.append(f"a verifier without its {w} check passes the corpus")
DROPPED = None
print(f"{weak} of {len(WEAKENINGS)} weakened verifiers are caught")
for f in fails:
print("FAIL", f)
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))