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]>
640 lines
24 KiB
Python
640 lines
24 KiB
Python
#!/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))
|