open-standards/wai/live-conformance/cache_authority_verify.py
Transaction Science 793ed1f2dc
All checks were successful
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 7s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 10s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 12s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 14s
WAI operation catalogue / does this change touch it (pull_request) Successful in 13s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 13s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 14s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 7s
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Successful in 6s
wai-fleet service / does this change touch it (pull_request) Successful in 14s
WAI neural sink gates / does this change touch it (pull_request) Successful in 13s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 12s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 9s
wai-ledger service / does this change touch it (pull_request) Successful in 14s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 14s
wai-key-proxy / does this change touch it (pull_request) Successful in 16s
WAI conformance / does this change touch it (pull_request) Successful in 31s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 32s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
WAI energy receipts / does this change touch it (pull_request) Successful in 33s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 8s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 7s
WAI Studio / does this change touch it (pull_request) Successful in 8s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 9s
wai-web wasm32 / does this change touch it (pull_request) Successful in 25s
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Successful in 5s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 55s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 1m11s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m58s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m22s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 1m51s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m32s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 2m31s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m51s
WAI neural sink gates / the exact JPEG-AI decoder reports its path (pinned inputs, required) (pull_request) Successful in 2m2s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m36s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 4m32s
wai-fleet service / service · linux (pull_request) Successful in 2m42s
wai-key-proxy / proxy · linux (pull_request) Successful in 57s
wai-pack / wai-pack · linux (pull_request) Successful in 3m19s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m30s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 27s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 5m59s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 4m11s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 5m56s
wai-ledger service / service · linux (pull_request) Successful in 1m50s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 7m18s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 1m7s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m48s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 7m43s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 3m41s
wai-fleet service / service · macos (pull_request) Successful in 48s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 3m39s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 4m54s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 3m2s
wai-pack / wai-pack · macos (pull_request) Successful in 3m27s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 5m55s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 13m52s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m28s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 16m34s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 11m38s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 27s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m7s
wai-ledger service / service · macos (pull_request) Successful in 1m6s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 9m35s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 10m56s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 17m20s
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Successful in 18m48s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 24m50s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 18m38s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 2m31s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 7m3s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m46s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 8m4s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m47s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 17m16s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 7m47s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 34m23s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 21s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 16s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 7m5s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 18s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 37m20s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 10m52s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 18m17s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 11m18s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 39m59s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 15s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 40m12s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 50s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m42s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 3m47s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 17m30s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 18m34s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 14m28s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 14m15s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 30m4s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h18m5s
wai/one-way-delivery, converted to the layout of the fix: no shared append point
The change of wai/one-way-delivery (f1258784d..987625394, 28 commits), on the fix branch: its
changelog entries as files of wai/changes/, its wai-conformance.yml edits as step files,
corpus files and test tags, and every other file as the branch has it.

- 25240c4f0 wai: timeliness — when each object of a group is of use, and whether each holder held it in time
- c84992362 wai: timeliness_verify.py — the timeliness corpus's second implementation
- 44036fcdc wai: timeliness in the registry, SPEC §5, the changelog and CI; http_receipts.py checks its claims
- 27ce6754c wai: claim_verify.py lists not-held-by-deadline, and the claim corpus holds a session that switches for it
- 73d8d8eb9 wai: timeliness corpus — each holder's own line in key order, a holder's earliest fallback, and §7.2–§7.3's unreached plan rules
- 64c735f0c wai: the files timeliness changes are clippy-clean — a let chain, and the fetch callback's type allowed
- b72dc36bf wai: rung price — what delivering a rung costs, by bearer and direction, and which option a comparer takes
- 4e905e213 wai: rung_price_verify.py — the rung price corpus's second implementation
- 711fa6126 wai: rung price in the registry, SPEC §5, the changelog and CI
- 8ab70654e wai: rung price corpus — a refusal for every form rule 1 and §6.1 set, and the neighbour order a plan gives
- 1bc465959 wai: rung price ranks energy by checked evidence — a class counts only on the seller's own per-hop records, current and under a confirmed key
- 9e97997d9 wai: rung_price — the evidence text bound at its exact sizes, and a key confirmed for another class confirming nothing
- 8f1168683 wai: rung price corpus — a key confirmed for another class, the uncertainty product, each edge and order of the evidence verdict, and the evidence a plan read
- 7330e26ff wai: rung price — §6.1 links jwp-receipts at its first mention, and §10 says a record's age is its room's word
- 12c099457 wai: cache authority — who served what a node holds, and on whose authority
- f8f5d3e09 wai: cache_authority_verify.py — the cache authority corpus's second implementation
- 354146619 wai: cache authority in the registry, SPEC §5, the changelog and CI; its corpus holds what §12 states
- 55c7f3031 wai: timeliness §6 reads holdings records alone; cache-authority §9 derives the lines that read every kind
- b8fc6f02b wai: cache authority's lines read records as timeliness does, its deadline claim meets the envelopes held, and its corpus fixes each order and instant
- 91c5291cf wai: the files cache authority changes are clippy-clean — a boxed envelope, the base's names named once, slices borrowed
- 8e479daff wai: a held envelope boxes both its forms, so neither outweighs the other
- 3ec8da807 wai: impression aggregates — ad presentations counted over a bearer with no return path
- 0433a35c3 wai: impression_aggregate_verify.py — the impression aggregates corpus's second implementation
- 7d8e150e4 wai: impression aggregates in the registry, SPEC §5, the changelog and CI; its corpus states each receipt whole and a control for every malformed file
- d5e8ace02 wai: impression aggregates' size bounds are checked at their edge, and §4.3's decides no outcome
- 4bc8e13bd wai: impression aggregates' table totals are exact, and a receipt given twice is checked when either occurrence was
- 3c9f20524 wai: key_log's two result enums keep their public form, and say so to clippy
- 987625394 wai: Item::with_authority and Count::instance_received build only with the feature that calls each

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 01:45:32 -05:00

940 lines
41 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 cache authority corpus (extensions/cache-authority.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, leaves and link hashes from the JWP corpus's
(`../jwp-conformance/jwp_verify.py`, jwp-receipts §2.2, §3); a claim's steps 1 to 5 from the
claim corpus's (`../claim-conformance/claim_verify.py`, jwp-receipts §3.1, whose steps 2 and 4
hold §5.1 and §5.2 here); a transmission's and a reconciliation's step 6 and the root over
leaves from the bearer corpus's (`bearer_verify.py`, bearer-receipts §4.3, §5.3); an
acknowledgement and its item lists from the settlement corpus's (`settlement_verify.py`,
settlement §3.6); a key log revision held under rules 1 to 3 from the signer corpus's
(`../signer-conformance/signer_verify.py`, jwp-receipts §7.2); the held-at statement's rule, a
holdings record held to its group, a timeliness claim against its group and timeliness lines
from the timeliness corpus's (`timeliness_verify.py`, timeliness §2, §4.3, §6); and region codes
from the rung price corpus's (`rung_price_verify.py`, rung-price §2). Everything this extension
adds is written here:
- §4.4: placing a server key by the heads a relying party names;
- §5.3: a cache-transmission claim against its group and the object receipts held;
- §7: the bearer verdict with the caches' instances beside the publisher's, the credit per
instance, the status, a statement offered, a sink's own instance and the energy bound;
- §8: the population, the tally's checks, the held-at statements of acknowledgements and counted
reconciliations, the count, its servers and the outcome;
- §9: timeliness lines that read acknowledgements and counted reconciliations too.
For every case it requires the outcome the corpus states, field for field; that every reason of
§11'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. A bound no vector carries is checked directly.
python3 cache_authority_verify.py <live-conformance/cache-authority>
Exit status 0 only if every case verifies, every mutated copy fails, every weakened verifier is
caught and every direct check holds. Python 3.8 or later; the `cryptography` package, which the
claim verifier imports.
"""
import copy
import hashlib
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
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
import bearer_verify as BV # noqa: E402
import settlement_verify as SET # noqa: E402
import timeliness_verify as TV # noqa: E402
import rung_price_verify as RP # noqa: E402
HV = TV.HV # the holdings corpus's reader, which the timeliness verifier loads
EXTENSION = os.path.join(HERE, "..", "extensions", "cache-authority.md")
JWP_RECEIPTS = os.path.join(HERE, "..", "extensions", "jwp-receipts.md")
SETTLEMENT = os.path.join(HERE, "..", "extensions", "settlement.md")
LAST_INSTANT = 253_402_300_799_999 # §2, timeliness §2: 9999-12-31T23:59:59.999Z
MAX_TARGET = 999_999 # §8.2 step 2
MAX_KEYS = 1 << 20 # §8.1: 1,048,576 keys
PPM = 1_000_000
POPULATION = b"wai:cache-population\x01" # §8.1
SERVING = ("cache/serve", "live/relay", "live/compositor", "live/ingest") # §4.4, in order
DECLARATION = 2 # settlement §3.1 `mode`
# A weakened run drops one of these checks (see `WEAKENINGS`); None checks everything.
DROPPED = None
def drops(name):
return DROPPED == name
def reasons_table():
"""§11's codes, in order, each with where the table says it is raised."""
text = TV.text_of(EXTENSION)
at = text.index("\n## 11. Reasons")
end = text.find("\n## ", at + 1)
rows = text[at:end].splitlines()
return [(r.split("`")[1], r.split("|")[2].strip()) for r in rows if r.startswith("| `")]
def need(ok, reason):
"""jwp-receipts §3.1 step 6."""
if not ok:
raise CV.Reject(6, reason)
def positions(hexbits):
"""§2 *Bitmap*: the positions whose bits are set, object i being bit 0x80 >> (i mod 8) of
byte ⌊i / 8⌋."""
bits = bytes.fromhex(hexbits)
return [i for i in range(8 * len(bits)) if bits[i // 8] & (0x80 >> (i % 8))]
def ceil_div(a, b):
return -(-a // b)
# ------------------------------------------------------------------ §8.3: instants
def issued_ms(t):
"""§8.3: a claim's `issued_at` as jwp-receipts §2.4.3 reads it, rounded up to the millisecond
(1000 × its seconds + ⌈f × 1000⌉, f its written fraction), and its whole seconds."""
s = JV.seconds(t)
m = re.match(r"\.([0-9]+)", t[19:])
digits = m.group(1) if m else ""
num, den = int(digits or "0"), 10 ** len(digits)
if drops("rec_second"):
ms = 1000 if num else 0
elif drops("rec_round"):
ms = num * 1000 // den
else:
ms = ceil_div(num * 1000, den)
return 1000 * s + ms, s
def ack_ms(end):
"""§8.3: an acknowledgement's window's last microsecond, `end` − 1, rounded up to the
millisecond, and its whole seconds."""
last = end if drops("ack_last_us") else end - 1
return (last // 1000 if drops("ack_round") else ceil_div(last, 1000)), last // 1_000_000
def before(word_s, start, check):
"""timeliness §2's rule for a statement before its group."""
return not drops(check) and TV.before_group(word_s, start)
# ------------------------------------------------------------------ §4.4: placing a server key
_HEADS = {}
def head(origin, text):
"""A head held for `origin` (jwp-receipts §7.2 rules 1 to 3), or None."""
k = (origin, text)
if k not in _HEADS:
try:
r = SG.parse_revision(text)
SG.check_held(r, origin)
except (SG.Stop, TypeError):
r = None
_HEADS[k] = r
return _HEADS[k]
def later(at_ms, until):
"""§4.4: `at_ms` is later than a retired entry's `until` when `at_ms` > `until` × 1000."""
u = SG.secs(until) * 1000
return at_ms >= u if drops("until_inclusive") else at_ms > u
def roles_of(e):
"""The serving roles an entry's `may_sign` holds, in §4.4's order."""
if drops("serving"):
return list(e["may_sign"])
return [r for r in SERVING if r in e["may_sign"]]
def place(key, at_ms, heads):
"""§4.4: the placement of the key `key` (hexadecimal) for an item at `at_ms`, by the heads
named, keyed by origin, as §4.4 writes it."""
origins = sorted(heads, key=lambda o: o.encode(), reverse=drops("least_origin"))
held = [(o, head(o, heads[o])) for o in origins]
listed = []
for o, r in held:
e = None if r is None else next((e for e in r["keys"] if e["key"] == key), None)
if e is not None:
listed.append((o, e))
first = []
revoked = next(((o, e) for o, e in listed if e["status"] == "revoked"), None)
if revoked is not None and not drops("revoked"):
first.append(f"refused reason=key_revoked origin={revoked[0]} "
f"compromised_since={revoked[1]['compromised_since']}")
unheld = next((o for o, r in held if r is None), None)
if unheld is not None and not drops("not_held"):
first.append(f"unconfirmed reason=key_log_not_held origin={unheld}")
if first:
return first[-1] if drops("revoked_first") else first[0]
for o, e in listed:
roles = roles_of(e)
in_force = e["status"] == "active" or (e["status"] == "retired"
and (drops("until") or not later(at_ms, e["until"])))
if roles and in_force:
out = f"confirmed origin={o} key_id={e['id']} role={roles[0]}"
return out + (f" until={e['until']}" if e["status"] == "retired" else "")
retired = next(((o, e) for o, e in listed if roles_of(e) and e["status"] == "retired"), None)
if retired is not None:
return f"refused reason=key_retired origin={retired[0]} until={retired[1]['until']}"
bare = next((o for o, e in listed if not roles_of(e)), None)
if bare is not None:
return f"refused reason=authority_not_granted origin={bare}"
return "unconfirmed reason=server_not_listed"
# ------------------------------------------------------------------ §5.3: step 6
def check_cache_transmission(c, g, objects):
"""§5.3 against a group receipt that passed jwp-receipts §3 steps 1 and 2 (as jwp_verify reads
it), and the object receipts held under it, of which those that pass §3 step 5 are used."""
b = c["body"]
need(drops("ct_subject") or JV.link(g).hex() == c["subject"], "cache_transmission_subject_mismatch")
need(drops("ct_publisher") or c["signer_pubkey"] != g["signer_pubkey"], "cache_transmission_by_publisher")
need(drops("ct_leaf_count") or b["leaf_count"] == g["leaf_count"], "cache_transmission_leaf_count_mismatch")
sent = positions(b["sent"])
if len(sent) == g["leaf_count"]:
root = bytes.fromhex(g["root_hash"])
else:
leaves = []
for i in sent:
o = next((o for o in objects if o["object_id"] == i and BV.included(o, g)), None)
if o is None and drops("ct_held"):
continue
need(o is not None, "cache_transmission_objects_not_held")
leaves.append(JV.leaf(o))
root = BV.merkle_root(leaves)
need(drops("ct_root") or root.hex() == b["sent_root"], "cache_transmission_root_mismatch")
def read_objects(receipts):
"""The object receipts given that read (jwp-receipts §2.4.2)."""
out = []
for o in receipts:
try:
out.append(JV.read_object(o))
except JV.Refuse:
pass
return out
def context_group(ctx):
"""§5.3: the group receipt a claim is checked against, after jwp-receipts §3 steps 1 and 2."""
if not (isinstance(ctx, dict) and ctx.get("kind") == "group") and not drops("context_kind"):
raise CV.Reject(6, "context_mismatch")
try:
g = JV.read_group(ctx)
JV.verify_group(g)
except JV.Refuse:
raise CV.Reject(6, "context_invalid")
return g
def claim_outcome(case):
"""A claim through jwp-receipts §3.1 steps 1 to 6 against the context the case gives, its
signer's previous claim the last of the earlier ones held."""
try:
held = {}
for wire in case["earlier"]:
try:
e = TV.claim_steps(wire)
held[e["signer_pubkey"]] = e
except CV.Reject:
pass
c = TV.claim_steps(case["claim"])
CV.check_chain(c, held.get(c["signer_pubkey"]))
if case["context"] is None and not drops("unchecked"):
return "unchecked"
if c["class"] != "cache-transmission":
raise CV.Reject(6, "context_mismatch")
check_cache_transmission(c, context_group(case["context"]), read_objects(case["objects"]))
return "accept"
except CV.Reject as r:
return f"reject step={r.step} reason={r.reason}"
# ------------------------------------------------------------------ §7: the verdict
class Verdict:
"""§7.2 over a case's inputs, with what §7.3, §8 and §9 read from it."""
def __init__(self, case, count_only):
self.g = TV.read_groups([{"group": case["group"], "objects": case["objects"]}])[0]
gr = self.g.receipt
objs = [ob for ob, _ in self.g.pairs]
# Step 2: the claims.
labels, accepted, held = [], [], {}
for wire in case["claims"]:
try:
c = TV.claim_steps(wire)
except CV.Reject as r:
labels.append(f"reject step={r.step} reason={r.reason}")
accepted.append(None)
continue
previous = held.get(c["signer_pubkey"])
held[c["signer_pubkey"]] = c
try:
CV.check_chain(c, previous)
if c["class"] == "transmission":
BV.check_transmission(c, gr)
elif c["class"] == "reconciliation":
BV.check_reconciliation(c, gr, objs)
elif c["class"] == "cache-transmission":
check_cache_transmission(c, gr, objs)
else:
raise CV.Reject(6, "context_mismatch")
except CV.Reject as r:
labels.append(f"reject step={r.step} reason={r.reason}")
accepted.append(None)
continue
labels.append(None)
accepted.append(c)
def by_instance(cls):
out = {}
for i, c in enumerate(accepted):
if c is not None and c["class"] == cls:
out.setdefault(c["body"]["bearer_id"], []).append(i)
return out
# Step 3: the publisher's declarations (bearer-receipts §6.2 step 4).
self.pub, self.pub_aside = {}, set()
for bid, idx in by_instance("transmission").items():
bodies = [accepted[i]["body"] for i in idx]
stands = all(b == bodies[0] for b in bodies)
if stands:
self.pub[bid] = bodies[0]
else:
self.pub_aside.add(bid)
for i in idx:
labels[i] = "declared" if stands else "set-aside reason=bearer_contradicted"
# Step 4: the caches' instances.
self.caches, self.cache_aside = {}, set()
for bid, idx in by_instance("cache-transmission").items():
cs = [accepted[i] for i in idx]
taken = (bid in self.pub or bid in self.pub_aside) and not drops("taken")
same = drops("cache_contradiction") or all(
(x["signer_pubkey"], x["body"]) == (cs[0]["signer_pubkey"], cs[0]["body"]) for x in cs)
if drops("same_claim"):
same = same and all(x["issued_at"] == cs[0]["issued_at"] for x in cs)
if not taken and not same:
self.cache_aside.add(bid)
if taken or not same:
for i in idx:
labels[i] = "set-aside reason=bearer_contradicted"
continue
self.caches[bid] = {"claims": cs, "body": cs[0]["body"], "server": cs[0]["signer_pubkey"],
"sent": positions(cs[0]["body"]["sent"])}
for i in idx:
labels[i] = "cache-declared"
# Step 5: the reconciliations.
counted, self.bits, self.instance_bits, self.counted = set(), {}, {}, []
for i, c in enumerate(accepted):
if c is None or c["class"] != "reconciliation":
continue
b, key = c["body"], c["signer_pubkey"]
bid = b["bearer_id"]
d, ci = self.pub.get(bid), self.caches.get(bid)
stated = d if d is not None else None if ci is None else ci["body"]
if bid in self.pub_aside or bid in self.cache_aside:
aside = "bearer_contradicted"
elif stated is None:
aside = "bearer_not_declared"
elif stated["bearer"] != b["bearer"]:
aside = "bearer_contradicted"
elif d is None and key == ci["server"] and not drops("self_ack"):
aside = "self_acknowledged"
elif count_only is not None and key not in count_only:
aside = "sink_not_confirmed"
else:
aside = None
if aside:
labels[i] = "set-aside reason=" + aside
continue
labels[i] = "joined reason=reconciliation_duplicate_sink" if (key, bid) in counted else "counted"
counted.add((key, bid))
got = positions(b["received"])
self.bits.setdefault(key, set()).update(got)
self.instance_bits.setdefault(bid, {}).setdefault(key, set()).update(got)
self.counted.append(i)
self.labels, self.accepted = labels, accepted
def outcome(self, case):
"""§7.2 step 6, §7.3 and §7.4, as the corpus states them."""
g, heads = self.g, case["heads"]
n = g.count
sinks = len(self.bits)
whole = sum(1 for s in self.bits.values() if len(s) == n)
received_by = [sum(1 for s in self.bits.values() if j in s) for j in range(n)]
def energy(e, k):
if e is None:
return None, None
j = e["joules_micro"]
share = j // max(1, k) if drops("energy_ceiling") else ceil_div(j, max(1, k))
return {"joules_micro": j, "class": e["energy_provenance"]}, share
bearers = []
for bid in sorted(self.pub, key=lambda s: s.encode()):
d = self.pub[bid]
k = len(self.instance_bits.get(bid, {}))
e, share = energy(d["energy"], k)
bearers.append({"bearer_id": bid, "bearer": d["bearer"], "sinks": k, "energy": e,
"per_receiver_upper_bound": share})
caches = []
for bid in sorted(self.caches, key=lambda s: s.encode()):
ci = self.caches[bid]
mine = self.instance_bits.get(bid, {})
if drops("per_instance"):
mine = {k: self.bits[k] for k in mine}
at = (min if drops("cache_earliest") else max)(issued_ms(x["issued_at"])[0] for x in ci["claims"])
if drops("cache_instant"):
at = g.start * 1000
e, share = energy(ci["body"]["energy"], len(mine))
sent = list(range(n)) if drops("sent_only") else ci["sent"]
caches.append({"bearer_id": bid, "bearer": ci["body"]["bearer"], "server": ci["server"],
"placement": place(ci["server"], at, heads), "sent": sent, "sinks": len(mine),
"received_by": [[j, sum(1 for s in mine.values() if j in s)] for j in sent],
"energy": e, "per_receiver_upper_bound": share})
transmitted = bool(self.pub) or (drops("cache_status") and bool(self.caches))
status = "reconciled" if sinks else "transmitted" if transmitted else "published"
sink = None
s = case["sink"]
if s is not None:
sid, sb = s["bearer_id"], s["bearer"]
if sid in self.pub_aside or sid in self.cache_aside:
sink = "bearer_contradicted"
elif sid in self.pub:
sink = "declared" if self.pub[sid]["bearer"] == sb else "bearer_contradicted"
elif sid in self.caches and not drops("sink_cache"):
sink = "cache-declared" if self.caches[sid]["body"]["bearer"] == sb else "bearer_contradicted"
else:
sink = "bearer_not_declared"
assertion = None
a = case["assertion"]
if a is not None:
if "transmitted" in a:
over = [d["bearer"] for d in self.pub.values()]
if drops("statement_publisher"):
over += [ci["body"]["bearer"] for ci in self.caches.values()]
assertion = "holds" if a["transmitted"] in over else "reject reason=bearer_not_declared"
elif whole == 0:
assertion = "reject reason=delivery_not_reconciled"
elif whole < a["delivered"]:
assertion = "reject reason=delivery_exceeds_reconciled"
else:
assertion = "holds"
return {"objects": g.verdicts, "claims": self.labels, "status": status, "sinks": sinks, "whole": whole,
"received_by": received_by, "bearers": bearers, "caches": caches, "sink": sink,
"assertion": assertion}
def count_only_of(case):
return None if case["count_only"] is None else set(case["count_only"])
def verdict(case):
try:
v = Verdict(case, count_only_of(case))
except JV.Refuse as r:
return "refuse reason=" + r.reason
return v.outcome(case)
# ------------------------------------------------------------------ §8: held by a deadline
def population_valid(region, keys):
"""§8.1: a region code of rung-price §2, and 1 to 2^20 keys in strictly increasing order of
their bytes."""
if not drops("region") and not RP.region_code(region):
return False
if not isinstance(keys, list) or len(keys) > MAX_KEYS or (not keys and not drops("population_empty")):
return False
if not all(CV.is_hex(k, 32) for k in keys):
return False
if drops("population_order"):
return True
raw = [bytes.fromhex(k) for k in keys]
return all(a < b or (a == b and drops("population_strict")) for a, b in zip(raw, raw[1:]))
def population_digest(region, keys):
"""§8.1: SHA-256 over the domain, the region's length and the region, n and the keys."""
r = region.encode("ascii")
head = POPULATION + (b"" if drops("digest_region") else bytes([len(r)]) + r)
return hashlib.sha256(head + len(keys).to_bytes(4, "big") + b"".join(bytes.fromhex(k) for k in keys)).hexdigest()
def verified(g, i):
"""§8.2 step 5: the content hash and length of object i held `delivered-verified`, or None."""
memo = g.__dict__.setdefault("ca_verified", {})
if i not in memo:
memo[i] = None
for ob, env in g.pairs:
if env is not None and ob["object_id"] == i:
try:
JV.accept(ob, g.receipt, env)
memo[i] = (ob["content_hash"].hex(), len(env))
break
except JV.Refuse:
pass
return memo[i]
def deadline_entry(wire, g, i):
"""§8.2 step 6: the entry for object i of a timeliness claim that stands against the group, with
no claim of its signer held before it, and the claim's link hash; or None."""
try:
c = TV.claim_steps(wire)
CV.check_chain(c, None)
if c["class"] != "timeliness":
raise CV.Reject(6, "context_mismatch")
held = g
if drops("deadline_envelopes"):
held = copy.copy(g)
held.envelopes = {}
TV.check_timeliness(c, held, {g.link: held})
except CV.Reject:
return None
entries = c["body"]["objects"]
e = next((e for e in entries if e["leaf_index"] == i), None)
if e is None and drops("deadline_entry"):
e = entries[0]
return None if e is None else (e, CV.link_hash(c))
_ACKS = {}
def read_ack(x):
"""settlement §3.6 for a record given, and its item lists against its entries: (reason or None,
record, items, the item lists' reason or None)."""
k = (x["record"], tuple(x["items"]))
if k not in _ACKS:
try:
a = SET.read_ack(bytes.fromhex(x["record"]))
except SET.Refuse as r:
_ACKS[k] = (r.reason, None, [], None)
return _ACKS[k]
items, why = [], None
try:
for j, e in enumerate(a["entries"]):
if j >= len(x["items"]):
raise SET.Refuse("record_missing")
raw = bytes.fromhex(x["items"][j])
its = SET.read_items(raw)
if not SET.recomputes(e, its, raw):
raise SET.Refuse("items_mismatch")
items += its
except SET.Refuse as r:
why, items = r.reason, []
_ACKS[k] = (None, a, items, why)
return _ACKS[k]
def acknowledgements(acks, g):
"""§8.3: each record's label, in order, with the record and its items where it reads."""
out = []
for x in acks:
why, a, items, items_why = read_ack(x)
if why is not None:
out.append(("reject reason=" + why, None, []))
elif a["mode"] == DECLARATION and not drops("declaration"):
out.append(("declaration", a, []))
elif items_why is not None:
out.append(("reject reason=" + items_why, None, []))
elif before(ack_ms(a["end"])[1], g.start, "ack_before"):
out.append(("before-group", a, items))
else:
out.append(("read", a, items))
return out
def covers(spans, length):
"""Whether the union of the byte ranges `spans` covers [0, length)."""
if drops("covers"):
return bool(spans)
at = 0
for s, e in sorted(spans):
if at >= length:
break
if s > at:
return False
at = max(at, e)
return at >= length
def held_at(v, acks, i, h, length, keys):
"""§8.3's held-at statements of acknowledgements and counted reconciliations for object i (its
content hash h and length), for the keys `keys` (None for every key): {key: [(instant,
servers)]}, and the statements set aside before the group, as (key, source)."""
g = v.g
out, aside = {}, []
def wanted(k):
return keys is None or k in keys or drops("population_only")
recs = v.counted
if drops("line_counted"):
recs = [x for x, c in enumerate(v.accepted) if c is not None and c["class"] == "reconciliation"]
for x in recs:
c = v.accepted[x]
k, b = c["signer_pubkey"], c["body"]
if not wanted(k) or i not in positions(b["received"]):
continue
ms, s = issued_ms(c["issued_at"])
if before(ms // 1000 if drops("rec_before_ms") else s, g.start, "rec_before"):
aside.append((k, "reconciliation"))
continue
bid, server = b["bearer_id"], None
if bid in v.pub:
server = g.signer
elif bid in v.caches and (i in v.caches[bid]["sent"] or drops("sent_server")):
server = v.caches[bid]["server"]
out.setdefault(k, []).append((ms, set() if server is None else {server}))
readable = [(a, items) for label, a, items in acks if label in ("read", "before-group")
or (drops("declaration") and label == "declaration")]
for k in sorted({a["downstream"].hex() for a, _ in readable}):
if not wanted(k):
continue
mine = [(a, items) for a, items in readable
if a["downstream"].hex() == k and any(c.hex() == h for c, _, _ in items)]
rest = []
for a, items in mine:
if before(ack_ms(a["end"])[1], g.start, "ack_before"):
aside.append((k, a["digest"].hex()))
else:
rest.append((a, items))
if drops("ack_digest"):
rest.sort(key=lambda p: p[0]["end"])
elif not drops("ack_order"):
rest.sort(key=lambda p: (p[0]["end"], p[0]["digest"]))
spans = []
for n, (a, items) in enumerate(rest):
if drops("union"):
spans = []
spans += [(s, e) for c, s, e in items if c.hex() == h]
if covers(spans, length):
upto = rest[n:n + 1] if drops("servers_all") else rest[:n + 1]
out.setdefault(k, []).append((ack_ms(a["end"])[0], {p[0]["upstream"].hex() for p in upto}))
break
return out, aside
def tally(case):
"""§8: the tally, or `refuse reason=R` for §8.2."""
region, keys, i = case["region"], case["population"], case["index"]
if not population_valid(region, keys):
return "refuse reason=population_invalid"
stated, target = case["deadline_ms"], case["miss_target_ppm"]
if case["deadline_claim"] is None and not (CV.is_int(stated)
and (stated <= LAST_INSTANT or drops("deadline_bound"))):
return "refuse reason=target_invalid"
if not (CV.is_int(target) and (target <= MAX_TARGET or drops("target_bound"))):
return "refuse reason=target_invalid"
try:
v = Verdict(case, None if drops("count_only") else set(keys))
except JV.Refuse as r:
return "refuse reason=" + r.reason
g = v.g
if g.start is None and not drops("group_start"):
return "refuse reason=context_invalid"
if not (CV.is_int(i) and (i < g.count or drops("out_of_range"))):
return "refuse reason=object_out_of_range"
obj = verified(g, i)
if obj is None:
if not drops("verified"):
return "refuse reason=object_not_verified"
obj = (g.included.get(i, ""), 0)
h, length = obj
deadline, link = stated, None
if case["deadline_claim"] is not None:
got = deadline_entry(case["deadline_claim"], g, i)
if got is None:
return "refuse reason=deadline_not_stated"
deadline, link = got[0]["by_ms"], got[1]
acks = acknowledgements(case["acknowledgements"], g)
statements, aside = held_at(v, acks, i, h, length, set(keys))
rlabels, kept = TV.records_step(case["records"], [g])
standing = [kept[x] for x in sorted(kept) if rlabels[x] == "stands"]
if not drops("records"):
for holder, _, _, at in TV.held_from_records(standing, g.link, i):
if holder in keys:
statements.setdefault(holder, []).append((at, set()))
# §8.3: a hit has a held-at statement at or before the deadline; §8.4: its servers.
counted_keys = sorted(statements) if drops("population_only") else keys
hits = {}
for k in counted_keys:
on = [(t, s) for t, s in statements.get(k, []) if (t < deadline if drops("hit_inclusive") else t <= deadline)]
if on:
hits[k] = (set().union(*(s for _, s in on)), any(not s for _, s in on), on)
n = len(keys)
misses = n - len(hits)
miss_ppm = misses * PPM // n if drops("miss_ceil") else ceil_div(misses * PPM, n)
servers = []
for s in sorted(set().union(set(), *(S for S, _, _ in hits.values())), key=bytes.fromhex):
crediting = [(k, S, U, on) for k, (S, U, on) in hits.items() if s in S]
at = (min if drops("server_earliest") else max)(t for _, _, _, on in crediting for t, ss in on if s in ss)
if drops("server_instant"):
at = g.start * 1000
placement = "publisher" if s == g.signer and not drops("publisher") else place(s, at, case["heads"])
sole = sum(1 for _, S, U, _ in crediting if S == {s} and (drops("sole_unattributed") or not U))
servers.append({"key": s, "placement": placement, "hits": len(crediting), "sole": sole})
met = miss_ppm < target if drops("target_inclusive") else miss_ppm <= target
return {"outcome": "met" if met else "refuse reason=miss_target_exceeded", "region": region,
"population_digest": population_digest(region, keys), "deadline_ms": deadline, "deadline_claim": link,
"n": n, "hits": len(hits), "misses": misses, "miss_ppm": miss_ppm,
"missing": [k for k in keys if k not in hits],
"set_aside": [{"key": k, "source": src, "reason": "held_before_group"} for k, src in sorted(aside)],
"servers": servers, "unattributed": sum(1 for S, _, _ in hits.values() if not S),
"acknowledgements": [label for label, _, _ in acks], "records": rlabels, "claims": v.labels}
# ------------------------------------------------------------------ §9: timeliness lines
def lines(case):
"""§9: timeliness §6.3's lines for object `index`. Records give held-at statements as
timeliness §6 reads them, for the primary and its fallback whatever envelopes are held, with a
line for each holder with a record that stands and has an entry for either group;
acknowledgements and counted reconciliations give them for each object of the group held
`delivered-verified`, the primary and a fallback in its group among them, with a line for each
key with one."""
try:
v = Verdict(case, count_only_of(case))
except JV.Refuse as r:
return "refuse reason=" + r.reason
g, i = v.g, case["index"]
got = deadline_entry(case["claim"], g, i)
if got is None:
return "reject reason=deadline_not_stated"
entry = got[0]
fb = entry["fallback"]
acks = acknowledgements(case["acknowledgements"], g)
rlabels, kept = TV.records_step(case["records"], [g])
standing = [kept[x] for x in sorted(kept) if rlabels[x] == "stands"]
holders = TV.line_holders(g.link, entry, standing)
if drops("line_fallback_holders"):
holders = [h for h in holders if any(TV.has_entry(r, g.link) for r in standing if r[0].hex() == h)]
primary = TV.held_from_records(standing, g.link, i)
if drops("line_records_verified") and verified(g, i) is None:
primary = []
fallback = [] if fb is None else TV.held_from_records(standing, fb["group"], fb["leaf_index"])
for j in range(g.count):
obj = verified(g, j)
if obj is None:
continue
statements, _ = held_at(v, acks, j, obj[0], obj[1], None)
for k, st in statements.items():
if k not in holders and not drops("line_keys"):
holders.append(k)
if j == i:
primary += [(k, g.link, i, t) for t, _ in st]
if fb is not None and fb["group"] == g.link and fb["leaf_index"] == j and not drops("line_fallback_acks"):
fallback += [(k, g.link, j, t) for t, _ in st]
return TV.lines(entry, holders, primary, fallback)
# ------------------------------------------------------------------ the corpus
def placement_outcome(case):
return place(case["key"], case["at_ms"], case["heads"])
RUNS = (("claims", claim_outcome), ("placements", placement_outcome), ("verdicts", verdict), ("tallies", tally),
("lines", lines))
KINDS = tuple(k for k, _ in RUNS)
def check(doc, quiet=False):
"""The failures of a corpus document: each case's outcome, and every reason of §11."""
fails = []
for kind, run in RUNS:
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,
JV.Refuse, SET.Refuse, SG.Stop, HV.Refused) as e:
got = {"error": repr(e)}
if got != c["outcome"]:
fails.append(c["name"])
if not quiet:
print(f"FAIL {kind} {c['name']}\n stated {json.dumps(c['outcome'])}\n got {json.dumps(got)}")
stated = []
for kind in KINDS:
for c in doc[kind]:
TV.strings(c["outcome"], stated)
for code, _ in reasons_table():
if not any(s == code or re.search(r"reason=" + re.escape(code) + r"(\s|$)", s) 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, kinds=KINDS):
return next(c for k in kinds for c in d[k] if c["name"] == name)
def m_until(d):
case(d, "retired_at_until")["at_ms"] += 1
def m_least(d):
c = case(d, "confirmed_least_origin")
c["outcome"] = c["outcome"].replace("operator.example", "other.example")
def m_receipt(d):
# Object 3's receipt held too: the claim's root over objects 1 and 3 then verifies.
c = case(d, "objects_not_held")
c["objects"].append(case(d, "sent_partial")["objects"][3])
def m_credit(d):
case(d, "per_instance_credit")["outcome"]["caches"][0]["received_by"][1][1] = 1
def m_status(d):
case(d, "cache_declared_only")["outcome"]["status"] = "transmitted"
def m_head(d):
case(d, "head_not_held", ("verdicts",))["heads"]["https://operator.example"] = d["heads"]["operator"]
def m_count_only(d):
case(d, "confirmed_keys_only")["count_only"] = None
def m_bound(d):
case(d, "energy_bound_after")["outcome"]["caches"][0]["per_receiver_upper_bound"] = 3_333_333
def m_population(d):
c = case(d, "met_by_bearer_and_peers")
c["population"] = list(reversed(c["population"]))
def m_region(d):
case(d, "region_country")["region"] = "XB"
def m_deadline(d):
case(d, "ack_last_us_at_deadline")["deadline_ms"] -= 1
def m_ppm(d):
case(d, "rounded_up")["outcome"]["miss_ppm"] = 333_333
def m_sole(d):
case(d, "two_peers_complete")["outcome"]["servers"][0]["sole"] = 1
def m_items(d):
case(d, "ack_any_class")["acknowledgements"][0]["items"] = []
def m_claim_link(d):
case(d, "deadline_from_timeliness")["outcome"]["deadline_claim"] = "00" * 32
def m_line(d):
case(d, "timeliness_line_from_ack")["outcome"][0]["held_ms"] += 1
def m_reason_gone(d):
d["claims"].remove(case(d, "leaf_count_wrong"))
return [m_until, m_least, m_receipt, m_credit, m_status, m_head, m_count_only, m_bound, m_population, m_region,
m_deadline, m_ppm, m_sole, m_items, m_claim_link, m_line, m_reason_gone]
WEAKENINGS = ["rec_second", "rec_round", "ack_last_us", "ack_round", "until_inclusive", "serving", "least_origin",
"revoked", "not_held", "revoked_first", "until", "ct_subject", "ct_publisher", "ct_leaf_count",
"ct_held", "ct_root", "context_kind", "unchecked", "taken", "cache_contradiction", "same_claim",
"self_ack", "energy_ceiling", "per_instance", "cache_instant", "sent_only", "cache_status",
"sink_cache", "statement_publisher", "region", "population_empty", "population_order",
"population_strict", "digest_region", "deadline_entry", "declaration", "ack_before", "rec_before",
"covers", "union", "servers_all", "sent_server", "population_only", "line_counted", "deadline_bound",
"target_bound", "count_only", "group_start", "out_of_range", "verified", "records", "hit_inclusive",
"miss_ceil", "server_instant", "publisher", "target_inclusive", "line_keys", "deadline_envelopes",
"ack_order", "ack_digest", "server_earliest", "cache_earliest", "rec_before_ms", "line_fallback_acks",
"line_records_verified", "line_fallback_holders"]
def direct_checks():
"""What no vector carries, and what this text states of the texts beside it."""
fails = []
# §8.1's bound: 2^20 keys are a population, and one more is not.
keys = [k.to_bytes(32, "big").hex() for k in range(1, MAX_KEYS + 2)]
if population_valid("XA", keys):
fails.append("a population of 2^20 + 1 keys is accepted")
keys.pop()
if not population_valid("XA", keys):
fails.append("a population of 2^20 keys is refused")
del keys
# §8.3: an acknowledgement's hit is `end` − 1 ≤ deadline_ms × 1000 in 64 bits, its rounded
# instant at or before the deadline the same test, at the last instant and the widest `end`.
if LAST_INSTANT * 1000 >= 1 << 64:
fails.append("the last instant in microseconds does not fit in 64 bits")
for end in (LAST_INSTANT * 1000 + 1, LAST_INSTANT * 1000 + 2, (1 << 64) - 1):
if (ack_ms(end)[0] <= LAST_INSTANT) != (end - 1 <= LAST_INSTANT * 1000):
fails.append(f"an acknowledgement ending at {end} reads two ways")
# jwp-receipts §3.1 lists §5.2's codes at step 4 and §5.3's at step 6; its §2.4.4 row admits a
# receipt subject, as the claim verifier does.
jwp = TV.text_of(JWP_RECEIPTS)
for code, where in reasons_table():
step = {"§5.2": 4, "§5.3": 6}.get(where.split(" ")[0])
if step is not None and f"| {step} | `{code}` |" not in jwp:
fails.append(f"jwp-receipts §3.1 does not list {code} at step {step}")
if "| `cache-transmission` | `claim` | `receipt`" not in jwp or CV.ADMITS.get("cache-transmission") != {"receipt"}:
fails.append("jwp-receipts §2.4.4 and the claim verifier differ on the class's subject")
# §4.4: the serving roles after `cache/serve` are those settlement §13 places.
text = TV.text_of(SETTLEMENT)
s13 = text[text.index("\n## 13."):]
for role in SERVING[1:]:
if f"`{role}`" not in s13:
fails.append(f"settlement §13 does not place {role}")
return fails
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}")
fails += direct_checks()
print(f"{sum(len(doc[k]) for k in KINDS)} cases ({', '.join(f'{len(doc[k])} {k}' for k in KINDS)}): "
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))