A stitched source (extensions/stitched-presentation.md, PR #470, which this branch merges): the stitcher's pod-input records (WSI1) and the players' claims (WSC1), verified by that extension with the tariff's ad scope as its signers. Its presentations bill as presented impressions, each line citing the accepted records and the presented own claims; its report is that extension's §7.5 statement unchanged, a form-1 receipt's own quartiles and spend beside the presented ones. The corpus bills two vectors of live-conformance/stitched/ and refuses a stitched source given twice and a stitcher outside the scope. Co-Authored-By: Claude Opus 5.5 <[email protected]>
936 lines
41 KiB
Python
936 lines
41 KiB
Python
#!/usr/bin/env python3
|
||
"""Independent verifier of the receipt-ledger corpus (extensions/receipt-ledger.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. Each source
|
||
is verified by the second implementation of the corpus of its own extension, each written from
|
||
its text alone: a room-session receipt by live_metering_verify.py (live-metering §5), a
|
||
settlement statement by settlement_verify.py (settlement §8), a package receipt by
|
||
../pack-conformance/pack_verify.py (packaging §9), an ads room by live_ads_verify.py with
|
||
ad_presentation_verify.py (live-ads §9, ad-presentation §9), and a stream stitched by others by
|
||
stitched_presentation_verify.py (stitched-presentation §7), whose statement the ledger reports as
|
||
it is. Everything else is here:
|
||
|
||
- the tariff (§4): reading, rules, both signatures, its scope;
|
||
- the invoice (§6): reading, rules, signature, chain;
|
||
- what each source contributes (§5.2), its placement in time (§5.3), and the checks across
|
||
sources (§7.2 step 6);
|
||
- every line, citation, energy row and amount recomputed (§6.2 to §6.6), compared in §7.2 step
|
||
7's order;
|
||
- the two reports (§8).
|
||
|
||
For every vector it requires the outcome the corpus states; of the corpus, that every reason of
|
||
§10 appears in it 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 receipt_ledger_verify.py <live-conformance/ledger>
|
||
python3 -I receipt_ledger_verify.py --document <verification document>
|
||
|
||
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 sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
|
||
sys.path.insert(0, os.path.join(HERE, "..", "pack-conformance"))
|
||
import _ed25519 as ED # noqa: E402
|
||
import live_rooms_verify as LRV # noqa: E402
|
||
import live_metering_verify as LMV # noqa: E402
|
||
import settlement_verify as SV # noqa: E402
|
||
import live_ads_verify as LAV # noqa: E402
|
||
import ad_presentation_verify as APV # noqa: E402
|
||
import pack_verify as PV # noqa: E402
|
||
import stitched_presentation_verify as STV # noqa: E402
|
||
|
||
EXTENSION = os.path.join(HERE, "..", "extensions", "receipt-ledger.md")
|
||
|
||
TARIFF_MAGIC, INVOICE_MAGIC = b"WLT1", b"WLI1" # §4.1, §6.1
|
||
TARIFF_DOMAIN = b"wai:ledger-tariff\x01" # §4.2
|
||
ACCEPT_DOMAIN = b"wai:ledger-accept\x01" # §4.2
|
||
INVOICE_DOMAIN = b"wai:ledger-invoice\x01" # §6.7
|
||
CITES_DOMAIN = b"wai:ledger-cites\x01" # §6.5
|
||
ZERO = bytes(32)
|
||
U64 = 2 ** 64 - 1
|
||
ROOM, DELIVERY, PACKAGE, ADS = 1, 2, 3, 4 # §6.3 source kinds
|
||
SESSION, HOP, STATEMENT, ACK, PACK, AD_RECEIPT, CLAIM, POD_INPUT, STITCHED_CLAIM = 1, 2, 3, 4, 5, 6, 7, 8, 9 # §5.2
|
||
KIND_NAMES = {ROOM: "room", DELIVERY: "delivery", PACKAGE: "package", ADS: "ads"}
|
||
STITCHED = "stitched" # §5.1: a source kind billed as ads
|
||
SCOPES = {1: "room_hops", 2: "delivery_hops", 3: "packaging", 4: "ad_decisions"} # §6.6
|
||
LABELS = {2: "ModelBased", 3: "Estimator", 4: "OnChipCounter", 5: "CalibratedInstrument"}
|
||
TAGS = {v: k for k, v in LABELS.items()}
|
||
CLASS_NAMES = ["live", "fetch", "fill", "ancillary"]
|
||
UNITS = { # §6.3: name, divisor
|
||
ROOM: [("publisher_us", 10 ** 6)] + [("subscriber_us_%d" % c, 10 ** 6) for c in range(9)]
|
||
+ [("composition_us", 10 ** 6), ("recorded_us", 10 ** 6), ("events_sent", 1)],
|
||
DELIVERY: [(n, 10 ** 9) for n in CLASS_NAMES],
|
||
PACKAGE: [("packages", 1), ("entries", 1), ("stored_bytes", 10 ** 9)],
|
||
ADS: [("presented_impressions", 1), ("billable_impressions", 1)],
|
||
}
|
||
|
||
SKIP = set()
|
||
|
||
|
||
class Refuse(Exception):
|
||
def __init__(self, reason, at):
|
||
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()
|
||
|
||
|
||
class R:
|
||
"""A cursor over little-endian fields; 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 ValueError("truncated")
|
||
s = self.b[self.p:self.p + n]
|
||
self.p += n
|
||
return s
|
||
|
||
def u(self, n):
|
||
return int.from_bytes(self.take(n), "little")
|
||
|
||
|
||
def head(b, magic):
|
||
"""§4.3's first steps."""
|
||
if len(b) < 4:
|
||
return "truncated"
|
||
if b[:4] != magic:
|
||
return "malformed"
|
||
if len(b) < 5:
|
||
return "truncated"
|
||
if b[4] != 1:
|
||
return "unknown_version"
|
||
return None
|
||
|
||
|
||
def capitals(c):
|
||
return len(c) == 3 and all(0x41 <= x <= 0x5A for x in c)
|
||
|
||
|
||
def ascending(xs):
|
||
return all(a < b for a, b in zip(xs, xs[1:]))
|
||
|
||
|
||
def verify_sig(key, domain, body, sig):
|
||
return None if "signatures" in SKIP else ED.verify(key, domain + body, sig)
|
||
|
||
|
||
# ---------------------------------------------------------------- §4 the tariff
|
||
|
||
def read_tariff(b):
|
||
bad = head(b, TARIFF_MAGIC)
|
||
if bad:
|
||
raise Refuse(bad, ["tariff"])
|
||
r = R(b, 5)
|
||
try:
|
||
t = {"seller": r.take(32), "buyer": r.take(32), "start": r.u(8), "end": r.u(8), "currency": r.take(3)}
|
||
t["room"] = [r.u(4) for _ in range(13)]
|
||
t["delivery"] = [r.u(8) for _ in range(4)]
|
||
t["package"] = [r.u(8) for _ in range(3)]
|
||
t["ads"] = [r.u(8) for _ in range(2)]
|
||
t["scope"] = [(r.u(1), r.take(32)) for _ in range(r.u(2))]
|
||
body_end = r.p
|
||
seller_sig, buyer_sig = r.take(64), r.take(64)
|
||
except ValueError:
|
||
raise Refuse("truncated", ["tariff"])
|
||
if r.p != len(b):
|
||
raise Refuse("malformed", ["tariff"])
|
||
rules = (t["start"] < t["end"] and capitals(t["currency"]) and t["seller"] != ZERO and t["buyer"] != ZERO
|
||
and t["seller"] != t["buyer"] and t["scope"] and ascending(t["scope"])
|
||
and all(1 <= k <= 4 and i != ZERO for k, i in t["scope"]))
|
||
if not rules and "tariff-rules" not in SKIP:
|
||
raise Refuse("tariff_invalid", ["tariff"])
|
||
body = b[:body_end]
|
||
bad = verify_sig(t["seller"], TARIFF_DOMAIN, body, seller_sig)
|
||
if bad:
|
||
raise Refuse(bad, ["tariff", "seller"])
|
||
bad = None if "buyer-signature" in SKIP else verify_sig(t["buyer"], ACCEPT_DOMAIN, body, buyer_sig)
|
||
if bad:
|
||
raise Refuse(bad, ["tariff", "buyer"])
|
||
t["digest"] = sha(body)
|
||
return t
|
||
|
||
|
||
def price(t, kind, unit):
|
||
return {ROOM: t["room"], DELIVERY: t["delivery"], PACKAGE: t["package"], ADS: t["ads"]}[kind][unit - 1]
|
||
|
||
|
||
# ---------------------------------------------------------------- §6 the invoice
|
||
|
||
def read_invoice(b, at):
|
||
bad = head(b, INVOICE_MAGIC)
|
||
if bad:
|
||
raise Refuse(bad, at)
|
||
r = R(b, 5)
|
||
try:
|
||
i = {"seller": r.take(32), "buyer": r.take(32), "tariff": r.take(32), "prev": r.take(32), "start": r.u(8),
|
||
"end": r.u(8), "currency": r.take(3)}
|
||
i["records"] = [(r.u(1), r.take(32)) for _ in range(r.u(4))]
|
||
i["lines"] = []
|
||
for _ in range(r.u(2)):
|
||
i["lines"].append({"kind": r.u(1), "source": r.take(32), "unit": r.u(1), "quantity": r.u(8), "price": r.u(8),
|
||
"amount": r.u(8), "cited": r.u(4), "cites": r.take(32)})
|
||
i["energy"] = [(r.u(1), r.u(1), r.u(4), r.u(8), r.u(4)) for _ in range(r.u(1))]
|
||
i["unmetered"] = [r.u(4) for _ in range(4)]
|
||
i["amount"] = r.u(8)
|
||
sig = r.take(64)
|
||
except ValueError:
|
||
raise Refuse("truncated", at)
|
||
if r.p != len(b):
|
||
raise Refuse("malformed", at)
|
||
if any(e[1] == 0 for e in i["energy"]) and "row-of-no-class" not in SKIP:
|
||
raise Refuse("classes_summed", at)
|
||
keys = [(x["kind"], x["source"], x["unit"]) for x in i["lines"]]
|
||
rules = (i["start"] < i["end"] and capitals(i["currency"]) and ascending(i["records"])
|
||
and all(1 <= k <= 9 for k, _ in i["records"]) and ascending(keys)
|
||
and all(x["kind"] in UNITS and 1 <= x["unit"] <= len(UNITS[x["kind"]]) and x["quantity"] > 0 and x["cited"] > 0
|
||
for x in i["lines"])
|
||
and ascending([(e[0], e[1]) for e in i["energy"]])
|
||
and all(1 <= e[0] <= 4 and 2 <= e[1] <= 5 and e[2] > 0 for e in i["energy"]))
|
||
if not rules and "invoice-rules" not in SKIP:
|
||
raise Refuse("invoice_invalid", at)
|
||
bad = verify_sig(i["seller"], INVOICE_DOMAIN, b[:-64], sig)
|
||
if bad:
|
||
raise Refuse(bad, at)
|
||
i["digest"] = sha(b[:-64])
|
||
return i
|
||
|
||
|
||
# ---------------------------------------------------------------- §5 sources
|
||
|
||
def figure(joules, cls):
|
||
"""(tag or None, joules, declared ppm): a figure as §6.6 counts it."""
|
||
if cls is None or joules == 0:
|
||
return (None, 0, 0)
|
||
return (cls[0], joules, cls[1] if cls[0] in (4, 5) else 0)
|
||
|
||
|
||
def room_source(s):
|
||
keys = {"kind", "room", "receipt", "hops", "descriptors", "compositions", "takes", "logs"}
|
||
if set(s) != keys or not isinstance(s["room"], dict) or set(s["room"]) != {"descriptor", "grants", "key_log_head", "chains"}:
|
||
raise Refuse("malformed", [])
|
||
doc = {"rooms": {"r": s["room"]}}
|
||
v = {"room": "r", "receipt": s["receipt"], "hops": s["hops"], "descriptors": s["descriptors"],
|
||
"compositions": s["compositions"], "takes": s["takes"], "logs": s["logs"]}
|
||
got = LMV.verify_vector(v, doc, LMV.Cache())
|
||
if got["outcome"] != "accept":
|
||
raise Refuse(got["reason"], got["at"])
|
||
d = LRV.read_descriptor(bytes.fromhex(s["room"]["descriptor"]))
|
||
rc = LMV.read_receipt(bytes.fromhex(s["receipt"]), d["room"])
|
||
u = rc["units"]
|
||
units = [u["publisher_us"]] + list(u["subscriber_us"]) + [u["composition_us"], u["recorded_us"], u["events_sent"]]
|
||
return {"kind": ROOM, "digest": rc["digest"], "room": d["room"], "t0": d["t0"], "start": rc["start"], "end": rc["end"],
|
||
"units": units, "currency": rc["currency"], "prices": rc["prices"],
|
||
"hops": [(h["record"], figure(h["joules"], h["class"])) for h in rc["hops"]]}
|
||
|
||
|
||
def delivery_source(s):
|
||
if set(s) != {"kind", "statement", "records", "proofs", "hops", "rooms"}:
|
||
raise Refuse("malformed", [])
|
||
doc = {"records": [{"record": r["record"], "items": r["items"]} for r in s["records"]],
|
||
"proofs": [{"proof": p} for p in s["proofs"]], "hops": [{"hop": h} for h in s["hops"]],
|
||
"rooms": {str(k): r for k, r in enumerate(s["rooms"])}}
|
||
v = {"statement": s["statement"], "rooms": [str(k) for k in range(len(s["rooms"]))], "withhold": [],
|
||
"extra": {"records": [], "proofs": []}}
|
||
got = SV.statement_outcome(doc, v)
|
||
if got["outcome"] != "accept":
|
||
raise Refuse(got["reason"], got["at"])
|
||
st = SV.read_statement(bytes.fromhex(s["statement"]))
|
||
given = {}
|
||
for r in s["records"]:
|
||
b = bytes.fromhex(r["record"])
|
||
given[sha(b[:-64])] = (b, [bytes.fromhex(x) for x in r["items"]])
|
||
acks = []
|
||
for d in st["records"]:
|
||
b, lists = given[d]
|
||
a = SV.read_ack(b)
|
||
a["item_lists"] = [SV.read_items(x) for x in lists]
|
||
acks.append(a)
|
||
hops = {}
|
||
for h in s["hops"]:
|
||
b = bytes.fromhex(h)
|
||
hops[sha(b[:-64])] = b
|
||
figs = []
|
||
for d in st["hops"]:
|
||
x = LMV.read_hop(hops[d], hops[d][5:37])
|
||
figs.append((d, figure(x["joules"], x["class"])))
|
||
return {"kind": DELIVERY, "digest": st["digest"], "statement": st, "acks": acks, "hops": figs}
|
||
|
||
|
||
def package_source(s):
|
||
if set(s) != {"kind", "receipt", "indexes"} or not isinstance(s["receipt"], str):
|
||
raise Refuse("malformed", [])
|
||
try:
|
||
r = PV.read_receipt(s["receipt"].encode("utf-8"))
|
||
PV.verify_receipt_v2(r, [bytes.fromhex(x) for x in s["indexes"]])
|
||
except PV.Refused as e:
|
||
raise Refuse(e.code, ["receipt"])
|
||
j = r["json"]
|
||
cls = None
|
||
if j["energy_provenance"] is not None:
|
||
tag = TAGS[j["energy_provenance"]]
|
||
cls = (tag, j["energy_uncertainty"]["relative_ppm"] if tag in (4, 5) else 0)
|
||
return {"kind": PACKAGE, "digest": bytes.fromhex(j["receipt_hash"]), "signer": bytes.fromhex(j["signer_pubkey"]),
|
||
"entries": j["total_entries"], "stored": j["stored_bytes"], "figure": figure(j["joules_micro"], cls)}
|
||
|
||
|
||
def ads_source(s):
|
||
keys = {"kind", "descriptor", "grants", "key_log", "chains", "policy", "pt0_ms", "receipts", "pods", "claims",
|
||
"creatives", "issuers"}
|
||
if set(s) != keys:
|
||
raise Refuse("malformed", [])
|
||
holder = {"creatives": s["creatives"], "issuers": s["issuers"]}
|
||
APV.held(holder)
|
||
v = dict(s, name="source")
|
||
got, _ = LAV.ads(v, judge=lambda vv, d, p: APV.Judge(holder, vv, p))
|
||
if got["outcome"] == "gap":
|
||
raise Refuse("source_gap", [])
|
||
if got["outcome"] != "accept":
|
||
raise Refuse(got["reason"], got["at"])
|
||
d = LRV.read_descriptor(bytes.fromhex(s["descriptor"]))
|
||
claims_required = "claim_facts" in got
|
||
breaks = {bytes.fromhex(b["digest"]): b["t"] for b in got["breaks"]}
|
||
claim_digests = [None if c["digest"] is None else bytes.fromhex(c["digest"]) for c in got.get("claim_facts", [])]
|
||
receipts = []
|
||
for o, fact in zip(s["receipts"], got["receipt_facts"]):
|
||
r = LAV.read_receipt(o)
|
||
imps = []
|
||
for k, i in enumerate(r["impressions"]):
|
||
billable = i["quartiles_reached"] >= 1
|
||
if claims_required:
|
||
c = fact["impressions"][k]
|
||
counts = c["verdict"] == "presented"
|
||
verdict = c["verdict"]
|
||
claim = claim_digests[c["claim"]] if counts and c["claim"] is not None else None
|
||
else:
|
||
counts, verdict, claim = billable, "beacons", None
|
||
imps.append({"slot": i["slot"], "ad_id": i["ad_id"], "price_micros": i["price_micros"],
|
||
"quartiles_reached": i["quartiles_reached"], "billable": billable, "counts": counts,
|
||
"verdict": verdict, "claim": claim})
|
||
cls = None
|
||
if r["class"] is not None:
|
||
tag = TAGS[r["class"]["label"]]
|
||
cls = (tag, r["class"].get("ppm", 0))
|
||
receipts.append({"identity": bytes.fromhex(fact["identity"]), "decider": r["signer_pubkey"], "cue": r["cue"],
|
||
"session": r["session"], "t": breaks[r["cue"]], "spend": r["budget_spent_micros"],
|
||
"counted_spend": fact.get("presented_spend_micros", r["budget_spent_micros"]),
|
||
"impressions": imps, "figure": figure(r["joules_micro"], cls)})
|
||
return {"kind": ADS, "room": d["room"], "t0": d["t0"], "claims_required": claims_required, "receipts": receipts,
|
||
"claims": [c for c in claim_digests if c is not None]}
|
||
|
||
|
||
def stitched_source(s, signers):
|
||
"""§5.1: a stitched source, verified as stitched-presentation §7 verifies, with the tariff's
|
||
ad scope as its signers (§4.4). That verification refuses nothing as a whole."""
|
||
keys = {"kind", "records", "claims", "manifests", "responses", "receipts", "creatives", "issuers"}
|
||
if set(s) != keys:
|
||
raise Refuse("malformed", [])
|
||
creatives, issuers, _ = STV.held({"creatives": s["creatives"], "issuers": s["issuers"], "signers": []})
|
||
records = [bytes.fromhex(x) for x in s["records"]]
|
||
inp = {"records": records, "claims": [bytes.fromhex(x) for x in s["claims"]],
|
||
"manifests": [bytes.fromhex(x) for x in s["manifests"]], "responses": [bytes.fromhex(x) for x in s["responses"]],
|
||
"receipts": s["receipts"], "creatives": creatives, "issuers": issuers, "signers": list(signers)}
|
||
got = STV.verify(inp)
|
||
claims = got["claim_facts"]
|
||
accepted = []
|
||
for b, f in zip(records, got["record_facts"]):
|
||
if f["verdict"] != "accepted":
|
||
continue
|
||
ads = [(a["presentations"], [bytes.fromhex(claims[j]["digest"]) for j in a["own_claims"]
|
||
if claims[j]["verdict"] == "presented" and claims[j]["digest"] is not None])
|
||
for a in f["ads"]]
|
||
accepted.append({"digest": bytes.fromhex(f["digest"]), "signer": STV.read_record(b)["signer"], "ads": ads})
|
||
return {"kind": STITCHED, "records": accepted, "claims": [bytes.fromhex(c["digest"]) for c in claims if c["digest"] is not None],
|
||
"statement": got}
|
||
|
||
|
||
# A source's verification is a pure function of the source, and the corpus verifies the same
|
||
# sources under every mutated copy and every weakened verifier (none of whose checks is a
|
||
# source's own): each is verified once.
|
||
_SOURCES = {}
|
||
|
||
|
||
def verify_source(s, signers):
|
||
"""§5.1: the source verified by its own extension, a stitched source with `signers`, or
|
||
Refuse with that extension's place."""
|
||
key = json.dumps([s, sorted(k.hex() for k in signers)], sort_keys=True)
|
||
if key not in _SOURCES:
|
||
try:
|
||
_SOURCES[key] = read_source(s, signers)
|
||
except Refuse as e:
|
||
_SOURCES[key] = e
|
||
got = _SOURCES[key]
|
||
if isinstance(got, Refuse):
|
||
raise Refuse(got.reason, got.at)
|
||
return got
|
||
|
||
|
||
def read_source(s, signers):
|
||
if not isinstance(s, dict):
|
||
raise Refuse("malformed", [])
|
||
if s.get("kind") == "stitched":
|
||
return stitched_source(s, signers)
|
||
readers = {"room": room_source, "delivery": delivery_source, "package": package_source, "ads": ads_source}
|
||
if s.get("kind") not in readers:
|
||
raise Refuse("malformed", [])
|
||
return readers[s["kind"]](s)
|
||
|
||
|
||
def heads(f):
|
||
"""§5.2."""
|
||
if f["kind"] == ROOM:
|
||
return [(SESSION, f["digest"])]
|
||
if f["kind"] == DELIVERY:
|
||
return [(STATEMENT, f["digest"])]
|
||
if f["kind"] == PACKAGE:
|
||
return [(PACK, f["digest"])]
|
||
if f["kind"] == STITCHED:
|
||
return [(POD_INPUT, r["digest"]) for r in f["records"]]
|
||
return [(AD_RECEIPT, r["identity"]) for r in f["receipts"]]
|
||
|
||
|
||
def contributed(f):
|
||
"""§5.2: every record a source contributes."""
|
||
out = set(heads(f))
|
||
if f["kind"] == ROOM:
|
||
out |= {(HOP, d) for d, _ in f["hops"]}
|
||
elif f["kind"] == DELIVERY:
|
||
out |= {(ACK, d) for d in f["statement"]["records"]} | {(HOP, d) for d in f["statement"]["hops"]}
|
||
elif f["kind"] == ADS:
|
||
out |= {(CLAIM, c) for c in f["claims"]}
|
||
elif f["kind"] == STITCHED:
|
||
out |= {(STITCHED_CLAIM, c) for c in f["claims"]}
|
||
return out
|
||
|
||
|
||
def in_scope(t, kind, i):
|
||
return "scope" in SKIP or (kind, i) in t["scope"]
|
||
|
||
|
||
def check_source(t, start, end, f):
|
||
"""§7.2 step 5 after the source's own verification: scope, one tariff (§3), period (§5.3)."""
|
||
def within(a, b):
|
||
return "period" in SKIP or (start <= a and b <= end)
|
||
if f["kind"] == ROOM:
|
||
if not in_scope(t, 1, f["room"]):
|
||
return "outside_scope"
|
||
if f["currency"] != bytes(3) and (f["currency"] != t["currency"] or f["prices"] != t["room"]) and "conflict" not in SKIP:
|
||
return "tariff_conflict"
|
||
if not within(f["t0"] + f["start"], f["t0"] + f["end"]):
|
||
return "outside_period"
|
||
elif f["kind"] == DELIVERY:
|
||
st = f["statement"]
|
||
if not in_scope(t, 2, st["party"]):
|
||
return "outside_scope"
|
||
if st["currency"] != bytes(3) and (st["currency"] != t["currency"] or st["prices"] != t["delivery"]) and "conflict" not in SKIP:
|
||
return "tariff_conflict"
|
||
if not within(st["start"], st["end"]):
|
||
return "outside_period"
|
||
elif f["kind"] == PACKAGE:
|
||
if not in_scope(t, 3, f["signer"]):
|
||
return "outside_scope"
|
||
elif f["kind"] == STITCHED:
|
||
return None # accepted only under the tariff's signers (§4.4); it states no time (§5.3)
|
||
else:
|
||
if not all(in_scope(t, 4, r["decider"]) for r in f["receipts"]):
|
||
return "outside_scope"
|
||
if "period" not in SKIP and not all(start <= f["t0"] + r["t"] < end for r in f["receipts"]):
|
||
return "outside_period"
|
||
return None
|
||
|
||
|
||
def overlapping(a, b):
|
||
return a[0] < b[1] and b[0] < a[1]
|
||
|
||
|
||
def bills_again(f, g):
|
||
"""§5.3: whether f bills what g, an earlier source, billed."""
|
||
if f["kind"] != g["kind"]:
|
||
return False
|
||
if f["kind"] == ROOM:
|
||
return f["room"] == g["room"] and overlapping((f["start"], f["end"]), (g["start"], g["end"]))
|
||
if f["kind"] == DELIVERY:
|
||
a, b = f["statement"], g["statement"]
|
||
return a["party"] == b["party"] and overlapping((a["start"], a["end"]), (b["start"], b["end"]))
|
||
if f["kind"] == ADS:
|
||
return f["room"] == g["room"] and any((x["cue"], x["session"]) == (y["cue"], y["session"])
|
||
for x in f["receipts"] for y in g["receipts"])
|
||
return False
|
||
|
||
|
||
def check_across(facts, previous):
|
||
"""§7.2 step 6: (the index, the reason) of the first source refused, or None."""
|
||
billed = set(previous["records"]) if previous else set()
|
||
seen = set()
|
||
for k, f in enumerate(facts):
|
||
hs = heads(f)
|
||
if "duplicate" not in SKIP and (any(h in seen for h in hs) or len(set(hs)) != len(hs)):
|
||
return k, "record_duplicate"
|
||
if "overlap" not in SKIP and any(bills_again(f, g) for g in facts[:k]):
|
||
return k, "record_overlap"
|
||
if "rebilled" not in SKIP and contributed(f) & billed:
|
||
return k, "record_rebilled"
|
||
seen |= set(hs)
|
||
return None
|
||
|
||
|
||
# ---------------------------------------------------------------- §6.2–§6.6 the figures
|
||
|
||
def free(c, grant):
|
||
return c in (2, 3) and grant == ZERO
|
||
|
||
|
||
def cites_digest(cited):
|
||
return sha(CITES_DOMAIN, b"".join(bytes([k]) + i for k, i in sorted(cited)))
|
||
|
||
|
||
def compute(t, facts):
|
||
"""The records, lines (with their cited records), rows, unmetered counts, amount and the sums
|
||
per (scope, tag); None when a figure does not fit its field."""
|
||
records = set()
|
||
for f in facts:
|
||
records |= contributed(f)
|
||
q = {}
|
||
|
||
def add(kind, source, unit, n, cites):
|
||
if n == 0:
|
||
return
|
||
e = q.setdefault((kind, source, unit), [0, set()])
|
||
e[0] += n
|
||
e[1] |= set(cites)
|
||
for f in facts:
|
||
if f["kind"] == ROOM:
|
||
for unit in range(1, 14):
|
||
add(ROOM, f["room"], unit, f["units"][unit - 1], [(SESSION, f["digest"])])
|
||
elif f["kind"] == DELIVERY:
|
||
st = f["statement"]
|
||
for c in range(1, 5):
|
||
n = sum(x["bytes"] for x in st["lines"] if x["kind"] == 1 and x["class"] == c and not free(c, x["payer"]))
|
||
if n == 0:
|
||
continue
|
||
cites = [(STATEMENT, f["digest"])]
|
||
cites += [(ACK, a["digest"]) for a in f["acks"]
|
||
if a["mode"] == 1 and any(e["class"] == c and not free(c, e["grant"]) for e in a["entries"])]
|
||
add(DELIVERY, st["party"], c, n, cites)
|
||
elif f["kind"] == PACKAGE:
|
||
cite = [(PACK, f["digest"])]
|
||
add(PACKAGE, f["signer"], 1, 1, cite)
|
||
add(PACKAGE, f["signer"], 2, f["entries"], cite)
|
||
add(PACKAGE, f["signer"], 3, f["stored"], cite)
|
||
elif f["kind"] == STITCHED:
|
||
for r in f["records"]:
|
||
cites = [(POD_INPUT, r["digest"])] + [(STITCHED_CLAIM, c) for _, cs in r["ads"] for c in cs]
|
||
add(ADS, r["signer"], 1, sum(p for p, _ in r["ads"]), cites)
|
||
else:
|
||
unit = 1 if f["claims_required"] else 2
|
||
for r in f["receipts"]:
|
||
counted = [i for i in r["impressions"] if i["counts"]]
|
||
cites = [(AD_RECEIPT, r["identity"])] + [(CLAIM, i["claim"]) for i in counted if i["claim"] is not None]
|
||
add(ADS, r["decider"], unit, len(counted), cites)
|
||
lines, total = [], 0
|
||
for (kind, source, unit) in sorted(q):
|
||
n, cited = q[(kind, source, unit)]
|
||
p = price(t, kind, unit)
|
||
a = n * p // UNITS[kind][unit - 1][1]
|
||
if n > U64 or a > U64:
|
||
return None
|
||
total += a
|
||
lines.append({"kind": kind, "source": source, "unit": unit, "quantity": n, "price": p, "amount": a,
|
||
"cited": len(cited), "cites": cites_digest(cited), "cited_records": sorted(cited)})
|
||
figs, seen = [], set()
|
||
for f in facts:
|
||
if f["kind"] == ROOM:
|
||
for d, fig in f["hops"]:
|
||
if d not in seen:
|
||
seen.add(d)
|
||
figs.append((1, fig))
|
||
for f in facts:
|
||
if f["kind"] == DELIVERY:
|
||
for d, fig in f["hops"]:
|
||
if d not in seen:
|
||
seen.add(d)
|
||
figs.append((2, fig))
|
||
for f in facts:
|
||
if f["kind"] == PACKAGE:
|
||
figs.append((3, f["figure"]))
|
||
elif f["kind"] == ADS:
|
||
figs += [(4, r["figure"]) for r in f["receipts"]]
|
||
rows, unmetered, sums = rows_of(figs)
|
||
if rows is None or total > U64:
|
||
return None
|
||
return {"records": sorted(records), "lines": lines, "energy": rows, "unmetered": unmetered, "amount": total, "sums": sums}
|
||
|
||
|
||
def rows_of(figs):
|
||
unmetered = [0, 0, 0, 0]
|
||
acc = {}
|
||
for s, (tag, joules, ppm) in figs:
|
||
if tag is None:
|
||
unmetered[s - 1] += 1
|
||
continue
|
||
e = acc.setdefault((s, tag), [0, 0, 0])
|
||
e[0] += 1
|
||
e[1] += joules
|
||
e[2] = max(e[2], ppm)
|
||
rows = [(s, tag) + tuple(acc[(s, tag)]) for s, tag in sorted(acc)]
|
||
if any(r[3] > U64 for r in rows):
|
||
return None, unmetered, {}
|
||
return rows, unmetered, {k: v[1] for k, v in acc.items()}
|
||
|
||
|
||
def summed_across(row, sums):
|
||
"""§6.6: a row whose joules sum its class and another class of its scope, not its own."""
|
||
s, tag, _, joules, _ = row
|
||
own = sums.get((s, tag))
|
||
if own is None or own == joules:
|
||
return False
|
||
others = [j for (sc, tg), j in sums.items() if sc == s and tg != tag]
|
||
for m in range(1, 1 << len(others)):
|
||
if own + sum(j for i, j in enumerate(others) if m >> i & 1) == joules:
|
||
return True
|
||
return False
|
||
|
||
|
||
def check_figures(inv, c):
|
||
"""§7.2 step 7, in its order."""
|
||
got = {(x["kind"], x["source"], x["unit"]): x for x in c["lines"]}
|
||
for x in inv["lines"]:
|
||
g = got.get((x["kind"], x["source"], x["unit"]))
|
||
if "quantity" not in SKIP and (g is None or x["quantity"] > g["quantity"]):
|
||
return "quantity_not_recorded"
|
||
if "summed" not in SKIP and any(summed_across(r, c["sums"]) for r in inv["energy"]):
|
||
return "classes_summed"
|
||
for x in inv["lines"]:
|
||
g = got.get((x["kind"], x["source"], x["unit"]))
|
||
if "citations" not in SKIP and g is not None and (x["cited"] != g["cited"] or x["cites"] != g["cites"]):
|
||
return "citation_mismatch"
|
||
strip = [{k: v for k, v in x.items() if k != "cited_records"} for x in c["lines"]]
|
||
if "mismatch" not in SKIP and (inv["records"] != c["records"] or inv["lines"] != strip or inv["energy"] != c["energy"]
|
||
or inv["unmetered"] != c["unmetered"] or inv["amount"] != c["amount"]):
|
||
return "invoice_mismatch"
|
||
return None
|
||
|
||
|
||
# ---------------------------------------------------------------- §8 the reports
|
||
|
||
def rows_json(rows):
|
||
return [{"class": LABELS[r[1]], "figures": r[2], "joules": r[3], "max_ppm": r[4]} for r in rows]
|
||
|
||
|
||
def ads_report(f):
|
||
receipts, nc = [], {}
|
||
tot = {"b": 0, "c": 0, "bs": 0, "cs": 0}
|
||
for r in f["receipts"]:
|
||
b = sum(1 for i in r["impressions"] if i["billable"])
|
||
c = sum(1 for i in r["impressions"] if i["counts"])
|
||
for i in r["impressions"]:
|
||
if i["billable"] and not i["counts"]:
|
||
nc[i["verdict"]] = nc.get(i["verdict"], 0) + 1
|
||
tot["b"] += b
|
||
tot["c"] += c
|
||
tot["bs"] += r["spend"]
|
||
tot["cs"] += r["counted_spend"]
|
||
receipts.append({
|
||
"identity": r["identity"].hex(), "decider": r["decider"].hex(), "cue": r["cue"].hex(), "session": r["session"].hex(),
|
||
"impressions": [{k: i[k] for k in ("slot", "ad_id", "price_micros", "quartiles_reached", "billable", "counts", "verdict")}
|
||
for i in r["impressions"]],
|
||
"billable_impressions": b, "billable_spend_micros": r["spend"], "counted_impressions": c,
|
||
"counted_spend_micros": r["counted_spend"]})
|
||
return {"room": f["room"].hex(), "claims_required": f["claims_required"], "receipts": receipts,
|
||
"billable_impressions": tot["b"], "counted_impressions": tot["c"], "billable_spend_micros": min(tot["bs"], U64),
|
||
"counted_spend_micros": min(tot["cs"], U64), "not_counted": dict(sorted(nc.items()))}
|
||
|
||
|
||
def overlaps_of(facts):
|
||
"""§8.2: by (content hash, downstream, first party, first record, second party, second record),
|
||
the bytes their items share."""
|
||
acks = [a for f in facts if f["kind"] == DELIVERY for a in f["acks"] if a["mode"] == 1]
|
||
out = {}
|
||
for x in acks:
|
||
for y in acks:
|
||
if not (x["upstream"] < y["upstream"] and x["downstream"] == y["downstream"]
|
||
and overlapping((x["start"], x["end"]), (y["start"], y["end"]))):
|
||
continue
|
||
for a in [i for l in x["item_lists"] for i in l]:
|
||
for b in [i for l in y["item_lists"] for i in l]:
|
||
if a[0] == b[0] and overlapping(a[1:], b[1:]):
|
||
k = (a[0], x["downstream"], x["upstream"], x["digest"], y["upstream"], y["digest"])
|
||
out[k] = min(out.get(k, 0) + min(a[2], b[2]) - max(a[1], b[1]), U64)
|
||
return out
|
||
|
||
|
||
def delivery_report(facts, c):
|
||
parties = {}
|
||
for f in facts:
|
||
if f["kind"] == DELIVERY:
|
||
parties.setdefault(f["statement"]["party"], []).append(f)
|
||
rows = []
|
||
for party in sorted(parties):
|
||
fs = parties[party]
|
||
classes = [[0, 0] for _ in range(4)]
|
||
free_bytes = stated = 0
|
||
declared = [0, 0, 0]
|
||
figs, seen = [], set()
|
||
currency = next((f["statement"]["currency"] for f in fs if f["statement"]["currency"] != bytes(3)), None)
|
||
for f in fs:
|
||
st = f["statement"]
|
||
for k in range(4):
|
||
classes[k][0] += st["classes"][k][0]
|
||
classes[k][1] += st["classes"][k][1]
|
||
free_bytes += sum(x["bytes"] for x in st["lines"] if x["kind"] == 1 and free(x["class"], x["payer"]))
|
||
stated += st["amount"]
|
||
for k in range(3):
|
||
declared[k] += st["unack"][k]
|
||
for d, fig in f["hops"]:
|
||
if d not in seen:
|
||
seen.add(d)
|
||
figs.append((2, fig))
|
||
energy, unmetered, _ = rows_of(figs)
|
||
rows.append({
|
||
"party": party.hex(), "statements": [f["digest"].hex() for f in fs],
|
||
"currency": None if currency is None else currency.decode("ascii"),
|
||
"classes": {CLASS_NAMES[k]: {"objects": classes[k][0], "bytes": classes[k][1]} for k in range(4)},
|
||
"free_bytes": free_bytes,
|
||
"declared": {"declarations": declared[0], "objects": declared[1], "bytes": declared[2]},
|
||
"stated_amount": stated,
|
||
"invoiced_amount": sum(x["amount"] for x in c["lines"] if x["kind"] == DELIVERY and x["source"] == party),
|
||
"energy": rows_json(energy or []), "unmetered": unmetered[1]})
|
||
ov = overlaps_of(facts)
|
||
overlaps = [{"content_hash": h.hex(), "downstream": d.hex(), "bytes": ov[(h, d, p1, r1, p2, r2)],
|
||
"first": {"party": p1.hex(), "record": r1.hex()}, "second": {"party": p2.hex(), "record": r2.hex()}}
|
||
for (h, d, p1, r1, p2, r2) in sorted(ov)]
|
||
return {"parties": rows, "overlaps": overlaps}
|
||
|
||
|
||
# ---------------------------------------------------------------- §7 verification
|
||
|
||
def unhex_field(doc, k):
|
||
v = doc.get(k)
|
||
if not isinstance(v, str) or not re.fullmatch(r"(?:[0-9a-f]{2})*", v):
|
||
raise Refuse("malformed", [k])
|
||
return bytes.fromhex(v)
|
||
|
||
|
||
def verify(doc):
|
||
"""§7.2: the outcome of a verification document."""
|
||
try:
|
||
tb, ib = unhex_field(doc, "tariff"), unhex_field(doc, "invoice")
|
||
pb = None if doc.get("previous") is None else unhex_field(doc, "previous")
|
||
sources = doc.get("sources")
|
||
if not isinstance(sources, list):
|
||
raise Refuse("malformed", ["source"])
|
||
t = read_tariff(tb) # step 1
|
||
inv = read_invoice(ib, ["invoice"]) # step 2
|
||
if "binding" not in SKIP and (inv["tariff"] != t["digest"] or inv["seller"] != t["seller"]
|
||
or inv["buyer"] != t["buyer"] or inv["currency"] != t["currency"]): # step 3
|
||
raise Refuse("tariff_mismatch", ["invoice"])
|
||
if "outside-tariff" not in SKIP and not (t["start"] <= inv["start"] and inv["end"] <= t["end"]):
|
||
raise Refuse("outside_tariff", ["invoice"])
|
||
prev = None
|
||
if inv["prev"] != ZERO: # step 4
|
||
if "previous-given" not in SKIP and (pb is None or sha(pb[:-64]) != inv["prev"]):
|
||
raise Refuse("record_missing", ["previous"])
|
||
if pb is not None:
|
||
prev = read_invoice(pb, ["previous"])
|
||
if prev["tariff"] != inv["tariff"] or prev["seller"] != inv["seller"] or prev["buyer"] != inv["buyer"]:
|
||
raise Refuse("tariff_mismatch", ["previous"])
|
||
if "previous-overlap" not in SKIP and prev["end"] > inv["start"]:
|
||
raise Refuse("invoice_overlap", ["previous"])
|
||
facts = []
|
||
for k, s in enumerate(sources): # step 5
|
||
try:
|
||
f = verify_source(s, [i for k, i in t["scope"] if k == 4])
|
||
except Refuse as e:
|
||
raise Refuse(e.reason, ["source", k] + list(e.at))
|
||
bad = check_source(t, inv["start"], inv["end"], f)
|
||
if bad:
|
||
raise Refuse(bad, ["source", k])
|
||
facts.append(f)
|
||
across = check_across(facts, prev) # step 6
|
||
if across:
|
||
raise Refuse(across[1], ["source", across[0]])
|
||
c = compute(t, facts) # step 7
|
||
if c is None:
|
||
raise Refuse("invoice_mismatch", ["invoice"])
|
||
bad = check_figures(inv, c)
|
||
if bad:
|
||
raise Refuse(bad, ["invoice"])
|
||
except Refuse as e:
|
||
return {"outcome": "refuse", "reason": e.reason, "at": e.at}
|
||
return accepted(inv, facts, c)
|
||
|
||
|
||
def accepted(inv, facts, c):
|
||
unmetered, coverage = {}, {}
|
||
for s in range(1, 5):
|
||
n = inv["unmetered"][s - 1]
|
||
unmetered[SCOPES[s]] = n
|
||
coverage[SCOPES[s]] = "partial" if n > 0 else "whole" if any(r[0] == s for r in inv["energy"]) else "none"
|
||
return {
|
||
"outcome": "accept",
|
||
"digest": inv["digest"].hex(),
|
||
"tariff": inv["tariff"].hex(),
|
||
"currency": inv["currency"].decode("ascii"),
|
||
"start": inv["start"],
|
||
"end": inv["end"],
|
||
"records": len(inv["records"]),
|
||
"lines": [{"source_kind": KIND_NAMES[x["kind"]], "source": x["source"].hex(), "unit": UNITS[x["kind"]][x["unit"] - 1][0],
|
||
"quantity": x["quantity"], "price": x["price"], "amount": x["amount"],
|
||
"cites": [[k, i.hex()] for k, i in x["cited_records"]]} for x in c["lines"]],
|
||
"energy": [{"scope": SCOPES[r[0]], "class": LABELS[r[1]], "figures": r[2], "joules": r[3], "max_ppm": r[4]}
|
||
for r in inv["energy"]],
|
||
"unmetered": unmetered,
|
||
"coverage": coverage,
|
||
"amount": inv["amount"],
|
||
"reports": {"ads": [ads_report(f) for f in facts if f["kind"] == ADS],
|
||
"stitched": [f["statement"] for f in facts if f["kind"] == STITCHED],
|
||
"delivery": delivery_report(facts, c)},
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------- the corpus
|
||
|
||
def document(doc, v):
|
||
return {"tariff": v["tariff"], "invoice": v["invoice"], "previous": v["previous"],
|
||
"sources": [doc["sources"][n] for n in v["sources"]]}
|
||
|
||
|
||
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(document(doc, v))
|
||
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."""
|
||
text = open(EXTENSION, encoding="utf-8").read()
|
||
at = text.index("\n## 10. Reasons")
|
||
end = text.find("\n## ", at + 1)
|
||
return [m.group(1) for m in re.finditer(r"^\| `([a-z_]+)` \|", text[at:end], re.M)]
|
||
|
||
|
||
def mutations(doc):
|
||
"""Copies of the corpus, one defect each, that a verifier must reject."""
|
||
def flip_hex(h, at):
|
||
b = bytearray(bytes.fromhex(h))
|
||
b[at % len(b)] ^= 0x01
|
||
return b.hex()
|
||
out = []
|
||
for k, v in enumerate(doc["vectors"]):
|
||
if v["outcome"]["outcome"] != "accept":
|
||
continue
|
||
for field, at in (("tariff", 6), ("tariff", -1), ("invoice", 40), ("invoice", -70), ("invoice", -1)):
|
||
m = copy.deepcopy(doc)
|
||
m["vectors"][k][field] = flip_hex(v[field], at)
|
||
out.append(("%s: %s byte %d flipped" % (v["name"], field, at), m))
|
||
m = copy.deepcopy(doc)
|
||
m["vectors"][k]["sources"] = v["sources"][:-1]
|
||
out.append(("%s: its last source dropped" % v["name"], m))
|
||
m = copy.deepcopy(doc)
|
||
m["vectors"][k]["outcome"]["amount"] += 1
|
||
out.append(("%s: its stated amount raised" % v["name"], m))
|
||
m = copy.deepcopy(doc)
|
||
m["vectors"][k]["outcome"]["lines"][0]["cites"] = []
|
||
out.append(("%s: its first line's citations dropped" % v["name"], m))
|
||
m = copy.deepcopy(doc)
|
||
rep = m["vectors"][0]["outcome"]["reports"]
|
||
rep["delivery"]["overlaps"] = []
|
||
out.append(("a_day: its overlaps dropped", m))
|
||
m = copy.deepcopy(doc)
|
||
m["vectors"][0]["outcome"]["reports"]["ads"][0]["not_counted"] = {}
|
||
out.append(("a_day: its ad report's not-counted figures dropped", m))
|
||
for k, v in enumerate(doc["vectors"]):
|
||
if v["outcome"]["outcome"] == "refuse":
|
||
m = copy.deepcopy(doc)
|
||
m["vectors"][k]["outcome"]["reason"] = "invoice_mismatch" if v["outcome"]["reason"] != "invoice_mismatch" else "malformed"
|
||
out.append(("%s: its stated reason changed" % v["name"], m))
|
||
break
|
||
return out
|
||
|
||
|
||
CHECKS = ["signatures", "buyer-signature", "tariff-rules", "invoice-rules", "row-of-no-class", "binding", "outside-tariff",
|
||
"previous-given", "previous-overlap", "scope", "conflict", "period", "duplicate", "overlap", "rebilled", "quantity",
|
||
"summed", "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"]}
|
||
listed = reasons_listed()
|
||
missing = [r for r in listed + ["truncated", "malformed", "unknown_version", "bad_key", "bad_signature", "record_missing",
|
||
"classes_summed"] if r not in seen]
|
||
if missing:
|
||
fails.append("no vector is refused %s" % missing)
|
||
print("%s every reason of §10 refuses a vector (%d listed)" % ("PASS" if not missing else "FAIL", len(listed)))
|
||
weak = []
|
||
for name, m in mutations(doc):
|
||
if not check(m, quiet=True):
|
||
weak.append(name)
|
||
print("%s every mutated copy of the corpus fails (%d copies)" % ("PASS" if not weak else "FAIL", len(mutations(doc))))
|
||
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_document(path):
|
||
"""One verification document (§7.1), such as a ledger service serves with an invoice: its
|
||
outcome, printed. Status 0 only when it is accepted."""
|
||
doc = json.load(open(path, encoding="utf-8"))
|
||
got = verify(doc)
|
||
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] == "--document":
|
||
sys.exit(check_document(sys.argv[2]))
|
||
if len(sys.argv) != 2:
|
||
print("usage: receipt_ledger_verify.py <live-conformance/ledger> | --document FILE", file=sys.stderr)
|
||
sys.exit(2)
|
||
sys.exit(main(sys.argv[1]))
|