open-standards/wai/live-conformance/carousel_verify.py
Transaction Science e2a7bfb87f wai: the carousel schedule — when each object passes on a bearer with no return path, and when a receiver holds it
On a bearer with no return path a receiver cannot ask for what it missed: it holds an object
only if the passes it heard carried enough encoding symbols. bearer-receipts declares the
transmission and reconciles it afterwards; nothing stated the plan in between.

extensions/carousel.md registers `wai.bearer.carousel` and the jwp-receipts claim class
`schedule` (§2.4.16): a publisher's signed plan, beside the group receipt and the instance's
transmission claim, naming for each object its forward error correction (RaptorQ, RFC 6330 with
its verified erratum 5548; Compact No-Code, RFC 5445) and that code's object transmission
information, the repair symbols each pass adds, whether later passes repeat their ESIs or send
new ones, and the passes' times and count. Step 4 holds each code's parameters to its bounds
and division into source blocks; step 6 holds the entries to the group, its objects' content
hashes and, where the bytes are held, their lengths.

From it a verifier derives, for a loss bound in parts per million, the pass after which a
receiver whose losses stay within it holds each object, for every pattern of loss within the
bound: a RaptorQ block at K' + h distinct symbols, the count RFC 6330 §5.8 states its decoding
failure bound at, and a symbol a repeating pass missed counted as missed again. It checks a
statement that an object or a group is held by an instant, sets aside schedules that contradict
each other or whose instance is undeclared or two-way, and a receiver derives the passes it
must listen to and how long its radio must be on.

The reference is the `carousel` module of wai-rs with the class's body in
`claim_receipt::schedule`; the corpus is live-conformance/carousel/ (33 verdicts, 50 claims, 16
plans) with carousel_verify.py as its second implementation and claim_verify.py's new
`carousel` mode for steps 1-4. tests/carousel.rs checks K' and Partition[] against an
independent RFC 6330 implementation (a dev-dependency) for every block size, and decodes each
RaptorQ object a case holds available from what a receiver within the bound holds. A receipt
index served over HTTP now checks a schedule claim against its group and listed objects.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 05:20:31 -05:00

491 lines
18 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.
Everything else this extension adds is written here:
- §4.3: a schedule against the group, its object receipts and their envelopes;
- §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.
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"):
sys.path.insert(0, os.path.join(HERE, "..", d))
import claim_verify as CV # noqa: E402
import jwp_verify as JV # 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()
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):
"""§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)]."""
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(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")
# ------------------------------------------------------------------ §5.3, §6.3
def needed(e, k, h):
"""§5.3: every source symbol for no-code, K′ + h for RaptorQ."""
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"] == "raptorq" and e["esi"] == "advance":
j = CV.ceil_div(need_, m - x)
elif e["fec"] == "raptorq":
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),
"failure_one_in": [100, 10_000, 1_000_000][h] if e["fec"] == "raptorq" 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 verdict(case):
bad = bounds(case["loss_ppm"], case["overhead"])
if bad:
return {"outcome": "refuse", "reason": bad}
try:
g = JV.read_group(case["group"])
JV.verify_group(g)
except JV.Refuse as r:
return {"outcome": "refuse", "reason": r.reason}
# 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)
# Step 2: each claim, §3.1 steps 1-5, then step 6.
held, claims, standing_claims = {}, [], []
for wire in case["claims"]:
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.
schedules = {}
for bid, idx in by_instance("schedule").items():
bodies = [standing_claims[i]["body"] for i in idx]
if not drops("schedule_contradiction") and any(b != bodies[0] for b in 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:
schedules[bid] = bodies[0]
# Step 5.
loss, h = case["loss_ppm"], case["overhead"]
lines = []
for bid in sorted(schedules):
for e in schedules[bid]["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"] == "raptorq" 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"],
}
# ------------------------------------------------------------------ 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)):
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"):
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") 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"))
return [m_pass, m_bytes, m_failure, m_loss, m_repair, m_statement, m_plan, m_held, m_accept, m_reason_gone]
WEAKENINGS = ["subject", "signer", "range", "before", "content", "length", "no_code_loss", "schedule_contradiction",
"undeclared", "bidirectional", "deadline", "holding", "late_pass", "again"]
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(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))