open-standards/wai/live-conformance/fleet_verify.py
Transaction Science 872502e8eb fleet_verify.py: §10 names every reason the corpus raises
A falsification (§10's report_mismatch row deleted) was caught by
tests/fleet.rs and not by the second implementation, which required every
listed reason to appear in the corpus but not every raised reason to be listed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:09:41 -04:00

855 lines
36 KiB
Python

#!/usr/bin/env python3
"""Independent verifier of the fleet corpus (extensions/fleet.md).
Written from the extension's text. It shares no code with wai-rs, which generates the corpus,
and never runs it. It checks Ed25519 signatures with ../claim-conformance/_ed25519.py, WAI's
strict rule (SPEC §7 *Signatures*) written from RFC 8032 with the standard library, and reads
per-hop records and their ledgers with live_metering_verify.py, the second implementation of
live metering, written from that text alone (§4.3). Everything else is here:
- the enrolment (§3): reading, rules, signature;
- the records and their carriage (§4.1): the relay's summary line and its text read strictly
(§4.2), the room descriptor as far as the fleet reads it (§4.4);
- placement in time and enrolment (§4.5), and one thing once (§4.6);
- the report (§6): reading, rules, signature;
- every line, row, unmetered count and citation recomputed (§5), compared in §7.2 step 6's
order, and the outcome of §7.3.
For every vector it requires the outcome the corpus states; of the corpus, that every reason of
§10 and every code it reuses appears in it, that §10 names every reason it raises, and that no other
file is there. 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 -I fleet_verify.py <live-conformance/fleet>
python3 -I fleet_verify.py --bundle <bundle.wai-fleet.json>
Exit status 0 only if every vector verifies, every mutated copy fails and every weakened verifier
fails. Python 3.8 or later; standard library only.
"""
import copy
import hashlib
import json
import os
import re
import struct
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
import _ed25519 as ED # noqa: E402
import live_metering_verify as LMV # noqa: E402
EXTENSION = os.path.join(HERE, "..", "extensions", "fleet.md")
ENROL_MAGIC, REPORT_MAGIC, DESCRIPTOR_MAGIC = b"WFE1", b"WFR1", b"WRD1" # §3.1, §6.1, §4.4
ENROL_DOMAIN = b"wai:fleet-enrol\x01" # §3.2
REPORT_DOMAIN = b"wai:fleet-report\x01" # §6.3
CITES_DOMAIN = b"wai:fleet-cites\x01" # §5.5
SUMMARY_DOMAIN = b"joule-moq-relay:session-summary\x01" # §4.2
DESCRIPTOR_DOMAIN = b"wai:live-room\x01" # §4.4
SUMMARY_FORMAT = "joule-moq-relay/session-summary/2"
LINE_HEAD, LINE_SIG, LINE_TAIL = b'{"summary":', b',"signature":"', b'"}'
BUNDLE_KIND = "wai-fleet-bundle" # §7.1
HOP, SUMMARY, DESCRIPTOR = 2, 10, 11 # §4.1
ROLES = {1: "relay", 2: "compositor", 3: "ingest", 4: "room", 5: "ledger", 6: "audit"} # §2
LABELS = {2: "ModelBased", 3: "Estimator", 4: "OnChipCounter", 5: "CalibratedInstrument"}
SCOPES = {1: "sessions", 2: "hops"}
U64, U32, U16 = 2 ** 64 - 1, 2 ** 32 - 1, 2 ** 16 - 1
HEX = re.compile(r"\A(?:[0-9a-f]{2})*\Z")
SKIP = set()
class Refuse(Exception):
def __init__(self, reason, at=None):
super().__init__(reason)
self.reason, self.at = reason, at
def sha(*parts):
h = hashlib.sha256()
for p in parts:
h.update(p)
return h.digest()
_VERIFIED = {}
def ed_verify(key, msg, sig):
"""ED.verify, remembered: the corpus is verified many times over (its mutated copies and its
weakened verifiers), and a signature's verdict depends on its bytes alone."""
k = (key, sha(msg), sig)
if k not in _VERIFIED:
_VERIFIED[k] = ED.verify(key, msg, sig)
return _VERIFIED[k]
def check_sig(name, key, domain, body, sig):
"""SPEC §7 *Signatures*: bad_key or bad_signature."""
if name in SKIP:
return
bad = ed_verify(key, domain + body, sig)
if bad:
raise Refuse(bad)
class R:
"""A cursor; running out is `truncated`."""
def __init__(self, b, p=0):
self.b, self.p = b, p
def take(self, n):
if self.p + n > len(self.b):
raise Refuse("truncated")
s = self.b[self.p:self.p + n]
self.p += n
return s
def u8(self):
return self.take(1)[0]
def u16(self):
return struct.unpack("<H", self.take(2))[0]
def u32(self):
return struct.unpack("<I", self.take(4))[0]
def u64(self):
return struct.unpack("<Q", self.take(8))[0]
def read_head(b, magic):
"""live-events §3.3's first two steps, with `magic` and version 1."""
if len(b) < 4:
raise Refuse("truncated")
if b[:4] != magic:
raise Refuse("malformed")
if len(b) < 5:
raise Refuse("truncated")
if b[4] != 1:
raise Refuse("unknown_version")
def hexb(s):
if not isinstance(s, str) or not HEX.match(s):
raise Refuse("malformed")
return bytes.fromhex(s)
# ---------------------------------------------------------------- §3 the enrolment
def read_enrolment(b):
read_head(b, ENROL_MAGIC)
r = R(b, 5)
e = {"organisation": r.take(32), "node": r.take(32), "role": r.u8(), "from": r.u64(), "until": r.u64()}
e["label"] = r.take(r.u8())
sig = r.take(64)
if r.p != len(b):
raise Refuse("malformed")
try:
text = e["label"].decode("utf-8")
label_ok = len(e["label"]) <= 64 and not any(ord(c) < 0x20 or ord(c) == 0x7F for c in text)
except UnicodeDecodeError:
label_ok = False
rules = 1 <= e["role"] <= 6 and e["from"] < e["until"] and label_ok and e["node"] != e["organisation"]
if not rules and "enrolment-rules" not in SKIP:
raise Refuse("enrolment_invalid")
check_sig("enrolment-signature", e["organisation"], ENROL_DOMAIN, b[:-64], sig)
e["digest"] = sha(b[:-64])
return e
# ---------------------------------------------------------------- §4 the records
class Int(int):
"""A number spelled as a plain non-negative decimal integer (§4.2)."""
class NotInt:
"""Any other number."""
def __init__(self, spelling):
self.spelling = spelling
def no_duplicates(pairs):
names = [k for k, _ in pairs]
if len(set(names)) != len(names) and "summary-duplicates" not in SKIP:
raise ValueError("a member named twice")
return dict(pairs)
def no_constant(s):
raise ValueError("not JSON: %s" % s)
def depth_ok(v, d=0):
"""No value inside more than 63 objects and arrays."""
if d >= 64:
return False
if isinstance(v, dict):
return all(isinstance(k, str) for k in v) and all(depth_ok(x, d + 1) for x in v.values())
if isinstance(v, list):
return all(depth_ok(x, d + 1) for x in v)
return True
def surrogate_free(v):
if isinstance(v, str):
return not any(0xD800 <= ord(c) <= 0xDFFF for c in v)
if isinstance(v, dict):
return all(surrogate_free(k) and surrogate_free(x) for k, x in v.items())
if isinstance(v, list):
return all(surrogate_free(x) for x in v)
return True
def strict_json(b):
"""§4.2: one JSON text read strictly, or Refuse("malformed")."""
try:
text = b.decode("utf-8")
v = json.loads(text, object_pairs_hook=no_duplicates, parse_constant=no_constant,
parse_int=lambda s: Int(s) if not s.startswith("-") else NotInt(s), parse_float=NotInt)
except (UnicodeDecodeError, ValueError, RecursionError):
raise Refuse("malformed")
if not depth_ok(v) or not surrogate_free(v):
raise Refuse("malformed")
return v
def integer(o, k):
v = o.get(k) if isinstance(o, dict) else None
if not isinstance(v, Int) or int(v) > U64:
raise Refuse("malformed")
return int(v)
def key_hex(v):
if not isinstance(v, str) or len(v) != 64 or not HEX.match(v):
raise Refuse("malformed")
return bytes.fromhex(v)
def obj(v):
if not isinstance(v, dict):
raise Refuse("malformed")
return v
def split_line(line):
"""§4.2: a summary's text and its signature, from its line in the relay's form."""
b = line.encode("utf-8") if isinstance(line, str) else None
if b is None:
raise Refuse("malformed")
if "summary-form" in SKIP:
b = b.rstrip()
sig_len = len(LINE_SIG) + 128
if not (b.startswith(LINE_HEAD) and b.endswith(LINE_TAIL)) or len(b) < len(LINE_HEAD) + sig_len + len(LINE_TAIL):
raise Refuse("malformed")
tail = b[len(b) - len(LINE_TAIL) - sig_len:len(b) - len(LINE_TAIL)]
if not tail.startswith(LINE_SIG) or not HEX.match(tail[len(LINE_SIG):].decode("latin-1")):
raise Refuse("malformed")
return b[len(LINE_HEAD):len(b) - len(LINE_TAIL) - sig_len], bytes.fromhex(tail[len(LINE_SIG):].decode("ascii"))
def read_summary(line):
"""§4.2: the line, the text, its members in the table's order, then its energy, its rules and
its signature."""
text, sig = split_line(line)
s = obj(strict_json(text))
if not isinstance(s.get("format"), str):
raise Refuse("malformed")
if s["format"] != SUMMARY_FORMAT and "summary-format" not in SKIP:
raise Refuse("unknown_version")
x = {"kind": SUMMARY, "relay": key_hex(s.get("relay_key")), "session": integer(s, "session"),
"opened": integer(s, "opened_unix_ms"), "closed": integer(s, "closed_unix_ms")}
energy = obj(s.get("relay_energy"))
if not isinstance(energy.get("acquisition"), str):
raise Refuse("malformed")
acquisition, joules = energy["acquisition"], integer(energy, "joules_micro")
tally = obj(s.get("tally"))
x["peer"] = key_hex(tally["peer"]) if "peer" in tally else None
x["bytes_in"] = integer(obj(tally.get("publisher")), "object_bytes_received")
sub = obj(tally.get("subscriber"))
forwarded = integer(sub, "object_bytes_forwarded")
fetched = 0
for t in obj(sub.get("tracks")).values():
fetched += integer(t, "fetch_bytes") if "fetch_bytes" in obj(t) else 0
two = len(energy) == 2
if acquisition == "unmetered" and joules == 0 and two:
x["figure"] = None
elif acquisition in ("ModelBased", "Estimator") and joules > 0 and two:
x["figure"] = (2 if acquisition == "ModelBased" else 3, joules, 0)
elif "summary-energy" in SKIP:
x["figure"] = None
else:
raise Refuse("energy_invalid")
x["bytes_out"] = forwarded + fetched
if (x["opened"] > x["closed"] or x["closed"] * 1000 > U64 or x["bytes_out"] > U64) and "summary-rules" not in SKIP:
raise Refuse("record_invalid")
check_sig("summary-signature", x["relay"], SUMMARY_DOMAIN, text, sig)
x["id"] = sha(text)
x["signer"] = x["relay"]
return x
_HOPS = {}
def lmv_read(rec, led):
"""live_metering_verify.py's reading of a record and its ledger, remembered by their bytes: the
reason it refuses them, or what it reads."""
if (rec, led) not in _HOPS:
try:
_HOPS[rec, led] = (None, LMV.read_hop(rec, rec[5:37]), LMV.read_ledger(led))
except LMV.Refuse as e:
_HOPS[rec, led] = (e.reason, None, None)
return _HOPS[rec, led]
def read_hop(rec, led):
"""§4.3: live metering's reading of the record and its ledger, and the ledger's binding."""
reason, h, entries = lmv_read(rec, led)
if reason:
raise Refuse(reason)
if (len(entries) != h["grants"] or LMV.ledger_root(entries) != h["ledger"]) and "ledger-binding" not in SKIP:
raise Refuse("ledger_mismatch")
cls = h["class"]
figure = None if cls is None else (cls[0], h["joules"], cls[1] if cls[0] in (4, 5) else 0)
return {"kind": HOP, "id": h["digest"], "signer": h["hop"], "room": h["room"], "role": h["role"], "start": h["start"],
"end": h["end"], "figure": figure, "sessions": h["sessions"], "sub": sum(h["sub"]),
"bytes_in": sum(e["bytes"] for e in h["tracks"] if e["dir"] == 1),
"bytes_out": sum(e["bytes"] for e in h["tracks"] if e["dir"] == 2)}
def read_descriptor(b):
"""§4.4: magic, version, authority, t0 and the signature; the digest is the room id."""
read_head(b, DESCRIPTOR_MAGIC)
if len(b) < 77 + 64:
raise Refuse("truncated")
check_sig("descriptor-signature", b[5:37], DESCRIPTOR_DOMAIN, b[:-64], b[-64:])
return {"kind": DESCRIPTOR, "id": sha(b[:-64]), "t0": struct.unpack("<Q", b[69:77])[0], "signer": None}
def read_record(c):
"""§4.1: a carriage, read as its kind."""
if not isinstance(c, dict):
raise Refuse("malformed")
k = c.get("kind")
if k == "hop" and set(c) == {"kind", "record", "ledger"}:
rec, led = hexb(c["record"]), hexb(c["ledger"])
return read_hop(rec, led)
if k == "summary" and set(c) == {"kind", "line"}:
return read_summary(c["line"])
if k == "descriptor" and set(c) == {"kind", "descriptor"}:
return read_descriptor(hexb(c["descriptor"]))
raise Refuse("malformed")
def instant(x, t0s):
"""§4.5: a summary's or a per-hop record's instant; None for one in no period."""
if x["kind"] == SUMMARY:
return x["closed"] * 1000
if x["room"] not in t0s:
if "descriptor" in SKIP:
return x["end"] - 1
raise Refuse("descriptor_missing")
t = t0s[x["room"]] + x["end"] - 1
return t if t <= U64 else None
def place(x, enrolments, t0s, period):
"""§7.2 step 4."""
e = next((e for e in enrolments if e["node"] == x["signer"]), None)
if e is None:
if "enrolled" in SKIP:
return
raise Refuse("signer_not_enrolled")
admits = e["role"] == 1 if x["kind"] == SUMMARY else (1 <= e["role"] <= 3 and x["role"] == e["role"])
if not admits and "role" not in SKIP:
raise Refuse("role_mismatch")
t = instant(x, t0s)
if not (t is not None and period[0] <= t < period[1]) and "period" not in SKIP:
raise Refuse("outside_period")
if not (t is not None and e["from"] <= t < e["until"]) and "enrolment-window" not in SKIP:
raise Refuse("outside_enrolment")
def once(records):
"""§4.6: the index of the first record that repeats one before it, and why."""
heads, sessions, spans = set(), set(), {}
for i, x in enumerate(records):
head = (x["kind"], x["id"])
if head in heads and "duplicate" not in SKIP:
raise Refuse("record_duplicate", ["record", i])
if "overlap" not in SKIP:
if x["kind"] == SUMMARY and (x["relay"], x["session"], x["opened"]) in sessions:
raise Refuse("record_overlap", ["record", i])
if x["kind"] == HOP and any(s < x["end"] and x["start"] < e for s, e in spans.get((x["signer"], x["room"]), [])):
raise Refuse("record_overlap", ["record", i])
heads.add(head)
if x["kind"] == SUMMARY:
sessions.add((x["relay"], x["session"], x["opened"]))
if x["kind"] == HOP:
spans.setdefault((x["signer"], x["room"]), []).append((x["start"], x["end"]))
# ---------------------------------------------------------------- §6 the report
def read_block(r):
rows = [{"scope": r.u8(), "tag": r.u8(), "figures": r.u32(), "joules": r.u64(), "max_ppm": r.u32()} for _ in range(r.u8())]
return {"rows": rows, "unmetered": [r.u32(), r.u32()]}
def block_ok(bl, room):
keys = [(x["scope"], x["tag"]) for x in bl["rows"]]
return (all(a < b for a, b in zip(keys, keys[1:]))
and all(x["scope"] in (1, 2) and 2 <= x["tag"] <= 5 and x["figures"] > 0 and (x["tag"] >= 4 or x["max_ppm"] == 0)
for x in bl["rows"])
and (not room or (all(x["scope"] != 1 for x in bl["rows"]) and bl["unmetered"][0] == 0)))
def read_report(b):
"""§6.4: the report, read, its rules, its signature."""
read_head(b, REPORT_MAGIC)
r = R(b, 5)
rep = {"organisation": r.take(32), "start": r.u64(), "end": r.u64()}
rep["records"] = [(r.u8(), r.take(32)) for _ in range(r.u32())]
rep["enrolments"] = [(r.take(32), r.take(32)) for _ in range(r.u16())]
nodes = []
for _ in range(r.u16()):
l = {"node": r.take(32), "role": r.u8()}
for k in ("sessions", "links", "bytes_in", "bytes_out", "link_bytes_in", "link_bytes_out"):
l[k] = r.u64()
l["hops"], l["hop_bytes_in"], l["hop_bytes_out"] = r.u32(), r.u64(), r.u64()
l["energy"], l["cited"], l["cites"] = read_block(r), r.u32(), r.take(32)
nodes.append(l)
rooms = []
for _ in range(r.u16()):
l = {"room": r.take(32), "t0": r.u64(), "hops": r.u32()}
for k in ("sessions", "subscriber_us", "bytes_in", "bytes_out"):
l[k] = r.u64()
l["energy"], l["cited"], l["cites"] = read_block(r), r.u32(), r.take(32)
rooms.append(l)
t = {}
for k in ("sessions", "links", "bytes_in", "bytes_out", "link_bytes_in", "link_bytes_out"):
t[k] = r.u64()
t["hops"], t["hop_bytes_in"], t["hop_bytes_out"] = r.u32(), r.u64(), r.u64()
t["energy"] = read_block(r)
sig = r.take(64)
if r.p != len(b):
raise Refuse("malformed")
rep["nodes"], rep["rooms"], rep["totals"] = nodes, rooms, t
blocks = [l["energy"] for l in nodes + rooms] + [t["energy"]]
if any(x["tag"] == 0 for bl in blocks for x in bl["rows"]) and "row-of-no-class" not in SKIP:
raise Refuse("classes_summed")
asc = lambda xs: all(a < b for a, b in zip(xs, xs[1:])) # noqa: E731
rules = (rep["start"] < rep["end"] and asc(rep["records"]) and all(k in (HOP, SUMMARY, DESCRIPTOR) for k, _ in rep["records"])
and asc([n for n, _ in rep["enrolments"]]) and asc([l["node"] for l in nodes])
and all(1 <= l["role"] <= 6 and block_ok(l["energy"], False) for l in nodes)
and asc([l["room"] for l in rooms]) and all(block_ok(l["energy"], True) for l in rooms)
and block_ok(t["energy"], False))
if not rules and "report-rules" not in SKIP:
raise Refuse("report_invalid")
check_sig("report-signature", rep["organisation"], REPORT_DOMAIN, b[:-64], sig)
rep["digest"] = sha(b[:-64])
return rep
# ---------------------------------------------------------------- §5 the figures
class Line:
"""A line's sums, before they are stated."""
def __init__(self):
self.n = [0] * 9
self.rows = {} # (scope, tag) -> [figures, joules, max_ppm]
self.unmetered = [0, 0]
self.cites = set()
self.role, self.t0 = 0, 0
def add(self, scope, figure):
if figure is None:
self.unmetered[scope - 1] += 1
return
tag, joules, ppm = figure
row = self.rows.setdefault((scope, tag), [0, 0, 0])
row[0] += 1
row[1] += joules
if tag >= 4:
row[2] = max(row[2], ppm)
def block(self):
rows = [{"scope": s, "tag": t, "figures": v[0], "joules": v[1], "max_ppm": v[2]} for (s, t), v in sorted(self.rows.items())]
if len(rows) > 255 or any(x["figures"] > U32 or x["joules"] > U64 for x in rows) or max(self.unmetered) > U32:
return None
return {"rows": rows, "unmetered": list(self.unmetered)}
def class_sums(self, scope):
return {t: v[1] for (s, t), v in self.rows.items() if s == scope}
def cites_digest(c):
return sha(CITES_DOMAIN, b"".join(bytes([k]) + i for k, i in sorted(c)))
def compute(enrolments, records):
"""§5: every line and the totals, from the records."""
roles = {e["node"]: e["role"] for e in enrolments}
t0s = {x["id"]: x["t0"] for x in records if x["kind"] == DESCRIPTOR}
nodes, rooms, total = {}, {}, Line()
for x in records:
if x["kind"] == SUMMARY:
l = nodes.setdefault(x["relay"], Line())
count, b_in, b_out = (1, 4, 5) if x["peer"] is not None else (0, 2, 3)
for y in (l, total):
y.n[count] += 1
y.n[b_in] += x["bytes_in"]
y.n[b_out] += x["bytes_out"]
l.add(1, x["figure"])
total.add(1, x["figure"])
l.cites.add((SUMMARY, x["id"]))
elif x["kind"] == HOP:
l = nodes.setdefault(x["signer"], Line())
for y in (l, total):
y.n[6] += 1
y.n[7] += x["bytes_in"]
y.n[8] += x["bytes_out"]
l.add(2, x["figure"])
total.add(2, x["figure"])
l.cites.add((HOP, x["id"]))
room = rooms.setdefault(x["room"], Line())
room.t0 = t0s.get(x["room"], 0)
for k, v in enumerate((1, x["sessions"], x["sub"], x["bytes_in"], x["bytes_out"])):
room.n[k] += v
room.add(2, x["figure"])
room.cites |= {(HOP, x["id"]), (DESCRIPTOR, x["room"])}
for node, l in nodes.items():
l.role = roles.get(node, 0)
return {"records": sorted((x["kind"], x["id"]) for x in records), "enrolments": sorted((e["node"], e["digest"]) for e in enrolments),
"nodes": nodes, "rooms": rooms, "total": total}
def stated(c):
"""The lines and totals as a report states them, or None for a figure no field holds."""
def fits(vals, top):
return all(v <= top for v in vals)
nodes = []
for node in sorted(c["nodes"]):
x = c["nodes"][node]
bl = x.block()
if bl is None or not fits(x.n[:6] + x.n[7:9], U64) or x.n[6] > U32 or len(x.cites) > U32:
return None
nodes.append({"node": node, "role": x.role, "sessions": x.n[0], "links": x.n[1], "bytes_in": x.n[2], "bytes_out": x.n[3],
"link_bytes_in": x.n[4], "link_bytes_out": x.n[5], "hops": x.n[6], "hop_bytes_in": x.n[7],
"hop_bytes_out": x.n[8], "energy": bl, "cited": len(x.cites), "cites": cites_digest(x.cites)})
rooms = []
for room in sorted(c["rooms"]):
x = c["rooms"][room]
bl = x.block()
if bl is None or not fits(x.n[1:5], U64) or x.n[0] > U32 or len(x.cites) > U32:
return None
rooms.append({"room": room, "t0": x.t0, "hops": x.n[0], "sessions": x.n[1], "subscriber_us": x.n[2], "bytes_in": x.n[3],
"bytes_out": x.n[4], "energy": bl, "cited": len(x.cites), "cites": cites_digest(x.cites)})
t = c["total"]
bl = t.block()
if bl is None or not fits(t.n[:6] + t.n[7:9], U64) or t.n[6] > U32:
return None
totals = {"sessions": t.n[0], "links": t.n[1], "bytes_in": t.n[2], "bytes_out": t.n[3], "link_bytes_in": t.n[4],
"link_bytes_out": t.n[5], "hops": t.n[6], "hop_bytes_in": t.n[7], "hop_bytes_out": t.n[8], "energy": bl}
return nodes, rooms, totals
def summed_across(row, sums):
"""§7.2 step 6.2: a row whose joules is its class's with at least one other class's of its scope."""
own = sums.get(row["tag"], 0)
if row["joules"] == own:
return False
others = [j for t, j in sums.items() if t != row["tag"]]
for m in range(1, 1 << len(others)):
if own + sum(j for i, j in enumerate(others) if m >> i & 1) == row["joules"]:
return True
return False
def compare(rep, c):
"""§7.2 step 6, in its order."""
for j, l in enumerate(rep["nodes"]):
if (l["node"] not in c["nodes"] or l["cited"] == 0) and "unsourced" not in SKIP:
raise Refuse("line_unsourced", ["report", "nodes", j])
for j, l in enumerate(rep["rooms"]):
if (l["room"] not in c["rooms"] or l["cited"] == 0) and "unsourced" not in SKIP:
raise Refuse("line_unsourced", ["report", "rooms", j])
blocks = [(l["energy"], c["nodes"].get(l["node"]), ["report", "nodes", j]) for j, l in enumerate(rep["nodes"])]
blocks += [(l["energy"], c["rooms"].get(l["room"]), ["report", "rooms", j]) for j, l in enumerate(rep["rooms"])]
blocks += [(rep["totals"]["energy"], c["total"], ["report", "totals"])]
blocks = [b for b in blocks if b[1] is not None]
if "summed" not in SKIP:
for bl, x, at in blocks:
if any(summed_across(row, x.class_sums(row["scope"])) for row in bl["rows"]):
raise Refuse("classes_summed", at)
if "unmetered" not in SKIP:
for bl, x, at in blocks:
if any(bl["unmetered"][s] < x.unmetered[s] for s in (0, 1)):
raise Refuse("unmetered_summed", at)
if "citations" not in SKIP:
for which, key in (("nodes", "node"), ("rooms", "room")):
for j, l in enumerate(rep[which]):
x = c[which].get(l[key])
if x is not None and (l["cited"] != len(x.cites) or l["cites"] != cites_digest(x.cites)):
raise Refuse("citation_mismatch", ["report", which, j])
s = stated(c)
same = s is not None and rep["records"] == c["records"] and rep["enrolments"] == c["enrolments"] \
and rep["nodes"] == s[0] and rep["rooms"] == s[1] and rep["totals"] == s[2]
if not same and "mismatch" not in SKIP:
raise Refuse("report_mismatch", ["report"])
# ---------------------------------------------------------------- §7 the bundle
def energy_json(bl):
out = {}
for s, name in SCOPES.items():
rows = [{"class": LABELS[x["tag"]], "figures": x["figures"], "joules_micro": x["joules"], "max_ppm": x["max_ppm"]}
for x in bl["rows"] if x["scope"] == s]
un = bl["unmetered"][s - 1]
cov = {(True, False): "whole", (True, True): "partial", (False, True): "unmetered", (False, False): "none"}[(bool(rows), un > 0)]
out[name] = {"rows": rows, "unmetered": un, "coverage": cov}
return out
def cites_json(c):
return [[k, i.hex()] for k, i in sorted(c)]
def outcome(rep, enrolments, records, c):
labels = {e["node"]: e["label"].decode("utf-8") for e in enrolments}
signed = {}
for x in records:
if x["signer"] is not None:
signed[x["signer"]] = signed.get(x["signer"], 0) + 1
t = rep["totals"]
return {
"outcome": "accept", "digest": rep["digest"].hex(), "organisation": rep["organisation"].hex(),
"start": rep["start"], "end": rep["end"], "records": len(rep["records"]),
"enrolments": [{"node": e["node"].hex(), "role": ROLES[e["role"]], "label": labels[e["node"]], "from": e["from"],
"until": e["until"], "digest": e["digest"].hex(), "records": signed.get(e["node"], 0),
"silent": signed.get(e["node"], 0) == 0} for e in sorted(enrolments, key=lambda e: e["node"])],
"nodes": [{"node": l["node"].hex(), "label": labels.get(l["node"]), "role": ROLES.get(l["role"]),
**{k: l[k] for k in ("sessions", "links", "bytes_in", "bytes_out", "link_bytes_in", "link_bytes_out", "hops",
"hop_bytes_in", "hop_bytes_out")},
"energy": energy_json(l["energy"]), "cites": cites_json(c["nodes"][l["node"]].cites)} for l in rep["nodes"]],
"rooms": [{"room": l["room"].hex(), **{k: l[k] for k in ("t0", "hops", "sessions", "subscriber_us", "bytes_in", "bytes_out")},
"energy": energy_json(l["energy"]), "cites": cites_json(c["rooms"][l["room"]].cites)} for l in rep["rooms"]],
"totals": {**{k: t[k] for k in ("sessions", "links", "bytes_in", "bytes_out", "link_bytes_in", "link_bytes_out", "hops",
"hop_bytes_in", "hop_bytes_out")}, "energy": energy_json(t["energy"])},
}
def verify(bundle):
"""§7.2, stopping at the first refusal; the outcome of §7.3."""
try:
return verify_steps(bundle)
except Refuse as e:
return {"outcome": "refuse", "reason": e.reason, "at": e.at}
def at(place):
"""Run a step's reading, its refusal placed."""
class Ctx:
def __enter__(self):
return self
def __exit__(self, kind, e, tb):
if isinstance(e, Refuse) and e.at is None:
e.at = place
return False
return Ctx()
def verify_steps(b):
with at(["bundle"]):
if not isinstance(b, dict) or set(b) != {"kind", "report", "enrolments", "records"} or b["kind"] != BUNDLE_KIND:
raise Refuse("malformed")
rb = hexb(b["report"])
if not isinstance(b["enrolments"], list) or not isinstance(b["records"], list):
raise Refuse("malformed")
eb = [hexb(e) for e in b["enrolments"]]
# 1. The report.
with at(["report"]):
rep = read_report(rb)
period = (rep["start"], rep["end"])
# 2. Each enrolment.
enrolments = []
for i, e in enumerate(eb):
with at(["enrolment", i]):
e = read_enrolment(e)
if e["organisation"] != rep["organisation"] and "organisation" not in SKIP:
raise Refuse("wrong_organisation")
if any(x["node"] == e["node"] for x in enrolments) and "enrolment-duplicate" not in SKIP:
raise Refuse("enrolment_duplicate")
if not (e["from"] < rep["end"] and rep["start"] < e["until"]) and "enrolment-period" not in SKIP:
raise Refuse("outside_period")
enrolments.append(e)
# 3. Each record reads and verifies.
records = []
for i, c in enumerate(b["records"]):
with at(["record", i]):
records.append(read_record(c))
# 4. Each summary and per-hop record is enrolled, of its role, placed, in the period and within
# its enrolment.
t0s = {x["id"]: x["t0"] for x in records if x["kind"] == DESCRIPTOR}
for i, x in enumerate(records):
if x["signer"] is not None:
with at(["record", i]):
place(x, enrolments, t0s, period)
# 5. Across records.
once(records)
# 6. The figures.
c = compute(enrolments, records)
compare(rep, c)
return outcome(rep, enrolments, records, c)
# ---------------------------------------------------------------- the corpus
def check(doc, quiet=False):
"""The failures: every vector whose stated outcome is not the one this verifier reaches."""
fails = []
for v in doc.get("vectors", []):
try:
got = verify(v["bundle"])
except Exception as e: # a crash on a mutated copy is a failure to verify it
got = {"outcome": "crash", "error": "%s: %s" % (type(e).__name__, e)}
ok = got == v.get("outcome")
if not quiet:
what = got["outcome"] + (" %s at %s" % (got["reason"], got["at"]) if got["outcome"] == "refuse" else "")
print(("PASS" if ok else "FAIL") + " %s: %s" % (v.get("name"), what))
if not ok:
fails.append(v.get("name"))
return fails
def reasons_listed():
"""§10's table, in order, and the codes its paragraph names as reused."""
text = open(EXTENSION, encoding="utf-8").read()
start = text.index("\n## 10. Reasons")
section = text[start:text.find("\n## ", start + 1)]
added = [m.group(1) for m in re.finditer(r"^\| `([a-z_]+)` \|", section, re.M)]
reused = [r for r in re.findall(r"`([a-z_]+)`", section.split("also raises", 1)[1]) if r not in added]
return added, reused
def flip_hex(h, at):
b = bytearray(bytes.fromhex(h))
b[at % len(b)] ^= 0x01
return b.hex()
def mutations(doc):
"""Copies of the corpus, one defect each, that a verifier must reject."""
out = []
def copy_with(name, k, edit):
m = copy.deepcopy(doc)
edit(m["vectors"][k])
out.append(("%s: %s" % (doc["vectors"][k]["name"], name), m))
for k, v in enumerate(doc["vectors"]):
if v["outcome"]["outcome"] != "accept":
continue
b = v["bundle"]
for at_ in (6, 50, -100, -1):
copy_with("report byte %d flipped" % at_, k, lambda x, a=at_: x["bundle"].__setitem__("report", flip_hex(b["report"], a)))
if b["enrolments"]:
copy_with("an enrolment's byte flipped", k, lambda x: x["bundle"]["enrolments"].__setitem__(0, flip_hex(b["enrolments"][0], 70)))
copy_with("its last enrolment dropped", k, lambda x: x["bundle"]["enrolments"].pop())
for i, r in enumerate(b["records"]):
if r["kind"] == "summary":
copy_with("record %d's line changed by a digit" % i, k,
lambda x, i=i: x["bundle"]["records"][i].__setitem__("line", re.sub(r'"session":(\d)', lambda m: '"session":%d' % ((int(m.group(1)) + 1) % 10), b["records"][i]["line"], count=1)))
elif r["kind"] == "hop":
copy_with("record %d's byte flipped" % i, k, lambda x, i=i: x["bundle"]["records"][i].__setitem__("record", flip_hex(b["records"][i]["record"], 120)))
if b["records"]:
copy_with("its last record dropped", k, lambda x: x["bundle"]["records"].pop())
copy_with("its first record given twice", k, lambda x: x["bundle"]["records"].append(b["records"][0]))
o = v["outcome"]
copy_with("its stated totals' bytes in raised", k, lambda x: x["outcome"]["totals"].__setitem__("bytes_in", o["totals"]["bytes_in"] + 1))
copy_with("its stated hops' coverage changed", k, lambda x: x["outcome"]["totals"]["energy"]["hops"].__setitem__(
"coverage", "whole" if o["totals"]["energy"]["hops"]["coverage"] != "whole" else "partial"))
copy_with("its stated sessions' unmetered figures set to zero", k, lambda x: x["outcome"]["totals"]["energy"]["sessions"].__setitem__(
"unmetered", 0 if o["totals"]["energy"]["sessions"]["unmetered"] else 1))
if o["enrolments"]:
copy_with("an enrolment stated silent the other way", k, lambda x: x["outcome"]["enrolments"][0].__setitem__("silent", not o["enrolments"][0]["silent"]))
if o["nodes"]:
copy_with("its first node line's citations dropped", k, lambda x: x["outcome"]["nodes"][0].__setitem__("cites", []))
for k, v in enumerate(doc["vectors"]):
if v["outcome"]["outcome"] == "refuse":
r = v["outcome"]["reason"]
copy_with("its stated reason changed", k, lambda x, r=r: x["outcome"].__setitem__("reason", "report_mismatch" if r != "report_mismatch" else "malformed"))
copy_with("its stated place changed", k, lambda x: x["outcome"].__setitem__("at", ["bundle"] if x["outcome"]["at"] != ["bundle"] else ["report"]))
return out
CHECKS = ["report-signature", "enrolment-signature", "summary-signature", "descriptor-signature", "report-rules", "row-of-no-class",
"enrolment-rules", "organisation", "enrolment-duplicate", "enrolment-period", "summary-form", "summary-duplicates",
"summary-format", "summary-energy", "summary-rules", "ledger-binding", "enrolled", "role", "descriptor", "period",
"enrolment-window", "duplicate", "overlap", "unsourced", "summed", "unmetered", "citations", "mismatch"]
def main(corpus_dir):
doc = json.load(open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8"))
fails = check(doc)
files = sorted(os.listdir(corpus_dir))
if files != ["README.md", "vectors.json"]:
fails.append("the corpus holds other files: %s" % files)
seen = {v["outcome"].get("reason") for v in doc["vectors"]}
added, reused = reasons_listed()
missing = [r for r in added + reused if r not in seen]
if missing:
fails.append("no vector is refused %s" % missing)
print("%s every reason of §10 and every code it reuses refuses a vector (%d added, %d reused)"
% ("PASS" if not missing else "FAIL", len(added), len(reused)))
unlisted = sorted(r for r in seen if r is not None and r not in added + reused)
if unlisted:
fails.append("§10 does not name %s, which the corpus raises" % unlisted)
print("%s §10 names every reason the corpus raises" % ("PASS" if not unlisted else "FAIL"))
copies = mutations(doc)
weak = [name for name, m in copies if not check(m, quiet=True)]
print("%s every mutated copy of the corpus fails (%d copies)" % ("PASS" if not weak else "FAIL", len(copies)))
fails += ["the mutated copy %r verifies" % w for w in weak]
undetected = []
for c in CHECKS:
SKIP.add(c)
try:
if not check(doc, quiet=True):
undetected.append(c)
finally:
SKIP.discard(c)
print("%s the corpus catches the verifier with each of its checks dropped (%d checks)" % ("PASS" if not undetected else "FAIL", len(CHECKS)))
fails += ["dropping the check %r goes unnoticed" % c for c in undetected]
for f in fails:
print("FAIL", f)
return 1 if fails else 0
def check_bundle(path):
"""One bundle, such as a fleet service exports with a report: its outcome, printed. Status 0
only when it is accepted."""
got = verify(json.load(open(path, encoding="utf-8")))
print(json.dumps(got, indent=1))
return 0 if got["outcome"] == "accept" else 1
if __name__ == "__main__":
if len(sys.argv) == 3 and sys.argv[1] == "--bundle":
sys.exit(check_bundle(sys.argv[2]))
if len(sys.argv) != 2:
print("usage: fleet_verify.py <live-conformance/fleet> | --bundle FILE", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))