open-standards/wai/live-conformance/settlement_verify.py
Transaction Science 82fa790c77 wai: influence corpus README, independent Python verifier, and settlement_verify reads attribution lines
influence_verify.py is the second implementation of extensions/influence.md: it verifies each
session with contribution_verify.py, regenerates every conditioned step, reads usage statements
with ai_usage_verify.py and statements with settlement_verify.py (now amended for kind 2 lines
and their bases), and computes every share, line and report itself. Registers
wai.media.influence (registry, wire identifiers, SPEC §5, CHANGELOG, CI step, export); the
settlement corpus's reserved-line vector becomes a line whose basis is not given.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 20:04:39 -04:00

860 lines
37 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 delivery settlement corpus (extensions/settlement.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
computes BLAKE3, its subtree chaining values and its parent nodes with
../claim-conformance/_blake3.py, written from the BLAKE3 specification: the module the HTTP head
proof's verifier reads its tree from. A room's descriptor, its grants and a grant's proof record
are read by the live-rooms corpus's own second implementation, live_rooms_verify.py (live-rooms
§3, §4, §10.5.1), and a per-hop record by the live-metering corpus's, live_metering_verify.py
(live-metering §3.6), each written from its text alone. Everything else is here:
- the acknowledgement and the declaration (§3, §4): reading, rules, signature, item lists, the
tree root of jwp-receipts §2.2, chains;
- the range proof (§5): reading, the covered chunks and the root over BLAKE3's chunk tree;
- a grant and its proof on an entry (§6.3);
- the statement (§7): reading, rules, signature, and every figure recomputed;
- verification in §8.2's order, with each refusal's place.
For every statement and range proof it requires the outcome the corpus states; of the corpus,
that every reason of §11 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 settlement_verify.py <live-conformance/settlement>
Exit status 0 only if every case 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 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 live_events_verify as LEV # noqa: E402
import live_rooms_verify as LRV # noqa: E402
import live_metering_verify as LMV # noqa: E402
import _ed25519 as ED # noqa: E402
import _blake3 as B3 # noqa: E402
EXTENSION = os.path.join(HERE, "..", "extensions", "settlement.md")
Refuse = LEV.Refuse
ACK_MAGIC, RANGE_MAGIC, STATEMENT_MAGIC = b"WDA1", b"WBR1", b"WSS1" # §3.1, §5.2, §7.1
ACK_DOMAIN = b"wai:settle-ack\x01" # §3.5
ITEMS_DOMAIN = b"wai:settle-items\x01" # §3.2
STATEMENT_DOMAIN = b"wai:settle-statement\x01" # §7.4
OBJECT_DOMAIN = b"moq-jwp:object\x01" # jwp-receipts §2.1
LEAF, NODE = b"moq-jwp:merkle-leaf\x01", b"moq-jwp:merkle-node\x01" # jwp-receipts §2.2
PROOF_DOMAIN = b"wai:live-room-proof\x01" # live-rooms §10.5.1
SEEK = 16 # live-rooms §3.3, bit 4
ACK, DECLARATION = 1, 2
HOP, SINK = 1, 2
LIVE, FETCH, FILL, ANCILLARY = 1, 2, 3, 4
DELIVERY_LINE, ATTRIBUTION_LINE = 1, 2 # §7.2
CLASS_NAMES = {LIVE: "live", FETCH: "fetch", FILL: "fill", ANCILLARY: "ancillary"}
RETRIEVAL = (FETCH, FILL)
LABELS = {2: "ModelBased", 3: "Estimator", 4: "OnChipCounter", 5: "CalibratedInstrument"}
ZERO = bytes(32)
U64 = 2 ** 64 - 1
CHUNK = 1024
DLEN = 15 # the length of jwp-receipts §2.1's domain
PER = 10 ** 9 # §7.3: prices per 10^9 bytes
SKIP = set()
# Signature checks are pure functions of their inputs, and the corpus checks the same records under
# every mutated copy and every weakened verifier: each is computed once.
_ED_VERIFY = ED.verify
_ED_MEMO = {}
def _ed_verify(pk, msg, sig, skip=()):
k = (bytes(pk), bytes(msg), bytes(sig), tuple(skip))
if k not in _ED_MEMO:
_ED_MEMO[k] = _ED_VERIFY(pk, msg, sig, skip=skip)
return _ED_MEMO[k]
ED.verify = _ed_verify
def reasons_of_extension():
"""§11's codes, read from the text."""
with open(EXTENSION, encoding="utf-8") as f:
text = f.read()
at = text.index("\n## 11. Reasons")
end = text.index("\n## ", at + 1)
return [line.split("`")[1] for line in text[at:end].splitlines() if line.startswith("| `")]
REASONS = reasons_of_extension()
REUSED = ["truncated", "malformed", "unknown_version", "bad_key", "bad_signature", "record_missing", "classes_summed",
"outside_grant_window", "grant_wrong_principal", "seek_not_granted", "pop_malformed", "pop_wrong_grant",
"pop_wrong_key", "pop_bad_signature", "pop_replayed"]
class Stop(Exception):
"""A refusal and its place (§8.2)."""
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()
def given_digest(b):
"""§8.2: SHA-256 over all but the last 64 bytes."""
return sha(b[:max(len(b) - 64, 0)])
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):
"""§3.6: the magic, then 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 check_sig(key, domain, body, sig):
bad = ED.verify(key, domain + body, sig)
if bad:
raise Refuse(bad)
# ---------------------------------------------------------------- §2, §3.4: the tree and items
def tree_root(hashes):
"""§2: jwp-receipts §2.2's root in its first form."""
if not hashes:
return ZERO
level = [B3.blake3(LEAF + h) for h in hashes]
while len(level) > 1:
if len(level) % 2:
level.append(level[-1])
level = [B3.blake3(NODE + level[k] + level[k + 1]) for k in range(0, len(level), 2)]
return level[0]
def read_items(b):
"""§3.6: an item list, or Refuse(items_invalid)."""
if len(b) % 48:
raise Refuse("items_invalid")
items = [(b[k:k + 32], int.from_bytes(b[k + 32:k + 40], "little"), int.from_bytes(b[k + 40:k + 48], "little"))
for k in range(0, len(b), 48)]
if "items-rules" not in SKIP:
if any(s >= e for _, s, e in items):
raise Refuse("items_invalid")
for x in range(len(items)):
for y in range(x + 1, len(items)):
if items[x][0] == items[y][0] and overlaps(items[x][1:], items[y][1:]):
raise Refuse("items_invalid")
return items
def overlaps(a, b):
return a[0] < b[1] and b[0] < a[1]
def recomputes(e, items, raw):
"""§3.6: the item list against its entry."""
if "items-recompute" in SKIP:
return True
return (len(items) == e["objects"] and sum(z - s for _, s, z in items) == e["bytes"]
and tree_root([h for h, _, _ in items]) == e["root"] and sha(ITEMS_DOMAIN, raw) == e["items"])
# ---------------------------------------------------------------- §3 the acknowledgement
def read_ack(b):
"""§3.6: a record, its rules, then its signature."""
read_head(b, ACK_MAGIC)
r = R(b, 5)
a = {"mode": r.u8(), "role": r.u8(), "upstream": r.take(32), "downstream": r.take(32), "start": r.u64(),
"end": r.u64(), "prev": r.take(32), "entries": []}
for _ in range(r.u16()):
a["entries"].append({"class": r.u8(), "grant": r.take(32), "proof": r.take(32), "objects": r.u32(),
"bytes": r.u64(), "root": r.take(32), "items": r.take(32)})
sig = r.take(64)
if r.p != len(b):
raise Refuse("malformed")
def entry_ok(e):
if e["class"] not in CLASS_NAMES or e["objects"] < 1 or e["bytes"] < e["objects"]:
return False
if e["class"] in RETRIEVAL:
return (e["grant"] == ZERO) == (e["proof"] == ZERO)
return e["proof"] == ZERO
keys = [(e["class"], e["grant"], e["proof"]) for e in a["entries"]]
rules = (a["mode"] in (ACK, DECLARATION) and a["role"] in (HOP, SINK) and a["start"] < a["end"]
and a["upstream"] != ZERO and (a["mode"] == DECLARATION or a["downstream"] != ZERO)
and a["entries"] and all(x < y for x, y in zip(keys, keys[1:])) and all(entry_ok(e) for e in a["entries"]))
if "ack-rules" not in SKIP and not rules:
raise Refuse("ack_invalid")
if "self-ack" not in SKIP and a["mode"] == ACK and a["downstream"] == a["upstream"]:
raise Refuse("self_acknowledged")
if "ack-signature" not in SKIP:
check_sig(a["upstream"] if a["mode"] == DECLARATION else a["downstream"], ACK_DOMAIN, b[:-64], sig)
a["digest"] = sha(b[:-64])
return a
# ---------------------------------------------------------------- §5 range proofs
def left(n):
"""§5.1: 1024 · 2^⌊log₂((n − 1)/1024)⌋, for n above 1024."""
q = (n - 1) // CHUNK
return CHUNK << (q.bit_length() - 1)
def covered(n, start, end):
"""§5.2: the covered chunks, as offsets into X."""
c0 = (DLEN + start) // CHUNK * CHUNK
c1 = min(DLEN + n, -(-(DLEN + end) // CHUNK) * CHUNK)
return c0, c1
def read_range(b):
"""§5.3."""
read_head(b, RANGE_MAGIC)
if len(b) < 62:
raise Refuse("truncated")
r = R(b, 5)
p = {"content_hash": r.take(32), "envelope_len": r.u64(), "start": r.u64(), "end": r.u64()}
n = r.u8()
if "range-bounds" not in SKIP:
if p["start"] >= p["end"]:
raise Refuse("range_invalid")
if p["end"] > p["envelope_len"] or DLEN + p["envelope_len"] > U64:
raise Refuse("range_outside_object")
c0, c1 = covered(p["envelope_len"], p["start"], p["end"])
want = 62 + 32 * n + (c1 - max(c0, DLEN))
if len(b) < want:
raise Refuse("truncated")
if len(b) > want:
raise Refuse("malformed")
p["cvs"] = [r.take(32) for _ in range(n)]
p["bytes"] = b[r.p:]
return p
def verify_range(p, h, audit):
"""§5.4, in order; the facts of §8.3."""
if "range-hash" not in SKIP and p["content_hash"] != h:
raise Refuse("range_proof_mismatch")
if audit is not None and "range-audit" not in SKIP:
if audit[0] != h or p["start"] < audit[1] or p["end"] > audit[2]:
raise Refuse("range_not_acknowledged")
L = DLEN + p["envelope_len"]
c0, c1 = covered(p["envelope_len"], p["start"], p["end"])
xc = (OBJECT_DOMAIN if c0 == 0 else b"") + p["bytes"]
if c0 == 0 and c1 == L:
if p["cvs"]:
raise Refuse("range_proof_mismatch")
root = B3.blake3(xc)
else:
cvs = list(p["cvs"])
def cv(s, n):
if s + n <= c0 or s >= c1:
if not cvs:
raise Refuse("range_proof_mismatch")
return cvs.pop(0)
if c0 <= s and s + n <= c1:
return B3.subtree_cv(xc[s - c0:s - c0 + n], s)
if n <= CHUNK:
raise Refuse("range_proof_mismatch")
k = left(n)
return B3.parent_cv(cv(s, k), cv(s + k, n - k))
k = left(L)
a = cv(0, k)
z = cv(k, L - k)
if cvs and "range-exact" not in SKIP:
raise Refuse("range_proof_mismatch")
root = B3.parent_cv(a, z, root=True)
if root != h and "range-root" not in SKIP:
raise Refuse("range_proof_mismatch")
return {"outcome": "accept", "start": p["start"], "end": p["end"], "envelope_len": p["envelope_len"],
"covered": [c0, c1], "length_established": c1 == L}
def range_outcome(v):
audit = None
if v["audit"] is not None:
audit = (bytes.fromhex(v["audit"]["content_hash"]), v["audit"]["start"], v["audit"]["end"])
try:
return verify_range(read_range(bytes.fromhex(v["proof"])), bytes.fromhex(v["content_hash"]), audit)
except Refuse as e:
return {"outcome": "refuse", "reason": e.reason}
# ---------------------------------------------------------------- §7 the statement
def read_statement(b):
"""§7.5: a statement, its rules, then its signature."""
read_head(b, STATEMENT_MAGIC)
r = R(b, 5)
s = {"party": r.take(32), "start": r.u64(), "end": r.u64()}
s["records"] = [r.take(32) for _ in range(r.u16())]
s["hops"] = [r.take(32) for _ in range(r.u16())]
s["currency"] = r.take(3)
s["prices"] = [r.u64() for _ in range(4)]
s["classes"] = [(r.u64(), r.u64()) for _ in range(4)]
s["unack"] = (r.u32(), r.u64(), r.u64())
s["unmetered"] = r.u16()
s["totals"] = [(r.u8(), r.u32(), r.u64()) for _ in range(r.u8())]
s["lines"] = []
for _ in range(r.u16()):
s["lines"].append({"kind": r.u8(), "payee": r.take(32), "class": r.u8(), "payer": r.take(32),
"objects": r.u64(), "bytes": r.u64(), "amount": r.u64()})
s["amount"] = r.u64()
sig = r.take(64)
if r.p != len(b):
raise Refuse("malformed")
if any(t[0] == 0 for t in s["totals"]):
raise Refuse("classes_summed")
def ascending(xs):
return all(x < y for x, y in zip(xs, xs[1:]))
tariff = all(p == 0 for p in s["prices"]) if s["currency"] == bytes(3) else all(0x41 <= c <= 0x5A for c in s["currency"])
keys = [(x["kind"], x["payee"], x["class"], x["payer"]) for x in s["lines"]]
rules = (s["start"] < s["end"] and s["records"] and ascending(s["records"]) and ascending(s["hops"]) and tariff
and all(2 <= t[0] <= 5 and t[1] > 0 for t in s["totals"]) and ascending([t[0] for t in s["totals"]])
and all(((x["kind"] == DELIVERY_LINE and x["payee"] == s["party"])
or (x["kind"] == ATTRIBUTION_LINE and x["payee"] != ZERO and x["payer"] != ZERO))
and x["class"] in CLASS_NAMES and x["objects"] > 0 for x in s["lines"])
and ascending(keys))
if "statement-rules" not in SKIP and not rules:
raise Refuse("statement_invalid")
if "statement-signature" not in SKIP:
check_sig(s["party"], STATEMENT_DOMAIN, b[:-64], sig)
s["digest"] = sha(b[:-64])
return s
def amount_of(prices, c, nbytes):
"""§7.3: ⌊bytes × p / 10^9⌋."""
return nbytes * prices[c - 1] // PER
def basis_order(s):
"""§8.2 step 6: the payers of the attribution lines, in the order the lines first name them."""
out = []
for x in s["lines"]:
if x["kind"] == ATTRIBUTION_LINE and x["payer"] not in out:
out.append(x["payer"])
return out
def attribution_figures(s, acks, bases):
"""§7.8: the attribution lines per (payee, class, basis), and each basis's deliveries per
(basis, class). `bases` maps a basis to its output, its whole and its payable buckets, each
(key, units, per_use, share_ppm)."""
lines, deliveries = {}, {}
for d in basis_order(s):
b = bases.get(d)
if b is None:
continue
for c in (LIVE, FETCH, FILL, ANCILLARY):
n = y = paid = 0
for a in acks:
if a["mode"] != ACK:
continue
held = False
for e, its in zip(a["entries"], a["items_read"]):
if e["class"] != c:
continue
free = c in RETRIEVAL and e["grant"] == ZERO
for h, start, end in its:
if h == b["output"]:
held = True
y += end - start
paid += 0 if free else end - start
n += 1 if held else 0
if n == 0:
continue
deliveries[(d, c)] = (n, y)
revenue = amount_of(s["prices"], c, paid)
for key, units, per_use, share in b["payees"]:
x = lines.setdefault((key, c, d), [n, y, 0])
x[2] += per_use * n + revenue * share * units // (10 ** 6 * b["whole"])
return lines, deliveries
def figures(s, acks, hops):
"""§7.6, §7.7: every figure, recomputed."""
lines = {}
unack = [0, 0, 0]
for a in acks:
if a["mode"] == DECLARATION:
unack[0] += 1
for e in a["entries"]:
unack[1] += e["objects"]
unack[2] += e["bytes"]
continue
for e in a["entries"]:
payer = e["grant"] if e["class"] in RETRIEVAL else a["downstream"]
x = lines.setdefault((e["class"], payer), [0, 0, 0])
x[0] += e["objects"]
x[1] += e["bytes"]
classes = [[0, 0] for _ in range(4)]
total = 0
for (c, payer), x in lines.items():
x[2] = 0 if (c in RETRIEVAL and payer == ZERO) else amount_of(s["prices"], c, x[1])
classes[c - 1][0] += x[0]
classes[c - 1][1] += x[1]
total += x[2]
totals, unmetered = {}, 0
for h in hops:
if h["class"] is None:
unmetered += 1
else:
t = totals.setdefault(h["class"][0], [0, 0])
t[0] += 1
t[1] += h["joules"]
return {"lines": lines, "classes": classes, "unack": unack, "unmetered": unmetered, "totals": totals, "amount": total}
def check_figures(s, f):
"""§7.6's comparison, in its order."""
delivery = [x for x in s["lines"] if x["kind"] == DELIVERY_LINE]
attribution = [x for x in s["lines"] if x["kind"] == ATTRIBUTION_LINE]
if "free-line" not in SKIP:
for x in delivery:
if x["class"] in RETRIEVAL and x["payer"] == ZERO and x["amount"] != 0:
raise Refuse("paid_without_grant")
if "bytes-covered" not in SKIP:
for x in delivery:
got = f["lines"].get((x["class"], x["payer"]))
if got is None or x["objects"] > got[0] or x["bytes"] > got[1]:
raise Refuse("bytes_not_acknowledged")
if "attribution-covered" not in SKIP:
for x in attribution:
got = f["deliveries"].get((x["payer"], x["class"]))
if got is None or x["objects"] > got[0] or x["bytes"] > got[1]:
raise Refuse("bytes_not_acknowledged")
if "classes-summed" not in SKIP:
tags = sorted(f["totals"])
for tag, _, joules in s["totals"]:
if tag in f["totals"] and joules != f["totals"][tag][1]:
others = [x for x in tags if x != tag]
for mask in range(1, 1 << len(others)):
extra = sum(f["totals"][others[i]][1] for i in range(len(others)) if mask >> i & 1)
if extra + f["totals"][tag][1] == joules:
raise Refuse("classes_summed")
if "figures" in SKIP:
return
same = (len(delivery) == len(f["lines"])
and all(f["lines"].get((x["class"], x["payer"])) == [x["objects"], x["bytes"], x["amount"]] for x in delivery)
and ("attribution-figures" in SKIP or (
len(attribution) == len(f["attribution"])
and all(f["attribution"].get((x["payee"], x["class"], x["payer"])) == [x["objects"], x["bytes"], x["amount"]]
for x in attribution)))
and [list(c) for c in s["classes"]] == f["classes"] and list(s["unack"]) == f["unack"]
and s["unmetered"] == f["unmetered"]
and len(s["totals"]) == len(f["totals"]) and all(f["totals"].get(t) == [n, j] for t, n, j in s["totals"])
and s["amount"] == f["amount"])
if not same:
raise Refuse("statement_mismatch")
# ---------------------------------------------------------------- §6.3 grants and proofs
def grant_check(a, e, rooms, proofs):
held = [g for r in rooms for g in r["grants"] if g["digest"] == e["grant"]]
if not held:
raise Refuse("record_missing")
g = held[0]
if "grant-window" not in SKIP and not (g["nbf"] * 10 ** 6 <= a["start"] and a["end"] <= g["exp"] * 10 ** 6):
raise Refuse("outside_grant_window")
if e["class"] not in RETRIEVAL:
if "grant-principal" not in SKIP and g["principal"] != a["downstream"]:
raise Refuse("grant_wrong_principal")
return
b = proofs.get(e["proof"])
if b is None:
raise Refuse("record_missing")
if "proof-checks" not in SKIP:
p = LRV.read_proof(b)
if p["grant"] != g["digest"]:
raise Refuse("pop_wrong_grant")
if p["key"] != g["principal"]:
raise Refuse("pop_wrong_key")
if ED.verify(p["key"], PROOF_DOMAIN + p["body"], p["sig"]) is not None:
raise Refuse("pop_bad_signature")
if "seek" not in SKIP and e["class"] == FETCH and not g["powers"] & SEEK:
raise Refuse("seek_not_granted")
# ---------------------------------------------------------------- §8 verification
def inputs_of(doc, v):
"""The pool, less what the vector withholds, with what it adds; its rooms."""
withhold = {bytes.fromhex(d) for d in v["withhold"]}
records, items, proofs, hops = {}, {}, {}, {}
for r in doc["records"] + v["extra"]["records"]:
b = bytes.fromhex(r["record"])
d = given_digest(b)
if d in withhold and "name" in r:
continue
records[d] = b
items[d] = [bytes.fromhex(x) for x in r["items"]]
for raw in [p["proof"] for p in doc["proofs"]] + v["extra"]["proofs"]:
b = bytes.fromhex(raw)
if sha(b) not in withhold:
proofs[sha(b)] = b
for h in doc["hops"]:
b = bytes.fromhex(h["hop"])
if given_digest(b) not in withhold:
hops[given_digest(b)] = b
rooms = [doc["rooms"][n] for n in v["rooms"]]
return records, items, proofs, hops, rooms
def verify_statement(doc, v, bases=None):
"""§8.2. `bases`, where given, maps each basis an attribution line may name to its figures
(§7.8), as influence §6.2 verifies them; a verifier of this extension alone holds none."""
bases = bases or {}
records, items, proofs, hops, room_inputs = inputs_of(doc, v)
# Step 1.
try:
s = read_statement(bytes.fromhex(v["statement"]))
except Refuse as e:
raise Stop(e.reason, ["statement"])
# Step 2.
rooms = []
for ri in room_inputs:
desc = bytes.fromhex(ri["descriptor"])
try:
d = LRV.read_descriptor(desc)
except Refuse as e:
raise Stop(e.reason, ["room", given_digest(desc).hex()])
held, err = LRV.check_grants(d, [bytes.fromhex(g) for g in ri["grants"]])
if err is not None and "room-grants" not in SKIP:
raise Stop(err[1], ["room", d["room"].hex(), "grant", err[0]])
rooms.append({"d": d, "grants": held or []})
# Step 3.
acks = []
for k, d in enumerate(s["records"]):
if d not in records:
raise Stop("record_missing", ["record", k])
try:
a = read_ack(records[d])
except Refuse as e:
raise Stop(e.reason, ["record", k])
if "upstream" not in SKIP and a["upstream"] != s["party"]:
raise Stop("ack_wrong_party", ["record", k])
if "period" not in SKIP and not (s["start"] <= a["start"] and a["end"] <= s["end"]):
raise Stop("ack_outside_period", ["record", k])
a["items_read"] = []
for j, e in enumerate(a["entries"]):
lists = items.get(d, [])
if j >= len(lists):
raise Stop("record_missing", ["record", k, "entry", j])
try:
its = read_items(lists[j])
except Refuse as x:
raise Stop(x.reason, ["record", k, "entry", j])
if not recomputes(e, its, lists[j]):
raise Stop("items_mismatch", ["record", k, "entry", j])
a["items_read"].append(its)
if e["grant"] != ZERO:
try:
grant_check(a, e, rooms, proofs)
except Refuse as x:
raise Stop(x.reason, ["record", k, "entry", j])
acks.append(a)
# Step 4.
chains = {}
for k, a in enumerate(acks):
chains.setdefault((a["mode"], a["upstream"], a["downstream"]), []).append(k)
before = {}
for ks in chains.values():
ks.sort(key=lambda k: (acks[k]["start"], k))
for p, q in zip(ks, ks[1:]):
before[q] = p
for k, a in enumerate(acks):
if k in before:
p = acks[before[k]]
if "chain-overlap" not in SKIP and p["end"] > a["start"]:
raise Stop("ack_overlap", ["record", k])
if "chain-prev" not in SKIP and a["prev"] != p["digest"]:
raise Stop("ack_chain_broken", ["record", k])
if "declared" not in SKIP:
for k, a in enumerate(acks):
if a["mode"] != DECLARATION or a["downstream"] == ZERO:
continue
mine = [x for its in a["items_read"] for x in its]
for b in acks:
if b["mode"] != ACK or b["downstream"] != a["downstream"] or not overlaps((a["start"], a["end"]), (b["start"], b["end"])):
continue
theirs = [x for its in b["items_read"] for x in its]
if any(x[0] == y[0] and overlaps(x[1:], y[1:]) for x in mine for y in theirs):
raise Stop("declared_and_acknowledged", ["record", k])
if "pop-replayed" not in SKIP:
named = {}
for k, a in enumerate(acks):
for j, e in enumerate(a["entries"]):
if e["proof"] == ZERO:
continue
if e["proof"] in named and named[e["proof"]] != k:
raise Stop("pop_replayed", ["record", k, "entry", j])
named.setdefault(e["proof"], k)
# Step 5.
held_hops = []
for k, d in enumerate(s["hops"]):
if d not in hops:
raise Stop("record_missing", ["hop", k])
b = hops[d]
try:
h = LMV.read_hop(b, b[5:37])
except Refuse as e:
raise Stop(e.reason, ["hop", k])
room = next((r for r in rooms if r["d"]["room"] == h["room"]), None)
if room is None:
raise Stop("record_missing", ["hop", k])
if "hop-party" not in SKIP and h["hop"] != s["party"]:
raise Stop("hop_not_party", ["hop", k])
t0 = room["d"]["t0"]
if "hop-span" not in SKIP and not (s["start"] <= t0 + h["start"] and t0 + h["end"] <= s["end"]):
raise Stop("span_outside_period", ["hop", k])
held_hops.append(h)
# Step 6: the bases.
if "bases" not in SKIP:
for k, d in enumerate(basis_order(s)):
if d not in bases:
raise Stop("record_missing", ["basis", k])
# Step 7: the figures.
f = figures(s, acks, held_hops)
f["attribution"], f["deliveries"] = attribution_figures(s, acks, {d: b for d, b in bases.items() if d in basis_order(s)})
try:
check_figures(s, f)
except Refuse as e:
raise Stop(e.reason, ["statement"])
coverage = "none" if not held_hops else ("whole" if s["unmetered"] == 0 else "partial")
out = {
"outcome": "accept",
"digest": s["digest"].hex(),
"currency": None if s["currency"] == bytes(3) else s["currency"].decode("ascii"),
"classes": {CLASS_NAMES[c]: {"objects": s["classes"][c - 1][0], "bytes": s["classes"][c - 1][1]} for c in (1, 2, 3, 4)},
"lines": [{"class": CLASS_NAMES[x["class"]], "payer": x["payer"].hex(), "objects": x["objects"], "bytes": x["bytes"],
"amount": x["amount"], "free": x["class"] in RETRIEVAL and x["payer"] == ZERO}
for x in s["lines"] if x["kind"] == DELIVERY_LINE],
"unacknowledged": {"declarations": s["unack"][0], "objects": s["unack"][1], "bytes": s["unack"][2]},
"coverage": coverage,
"unmetered": s["unmetered"],
"totals": [{"class": LABELS[t], "figures": n, "joules": j} for t, n, j in s["totals"]],
"amount": s["amount"],
}
attribution = [{"payee": x["payee"].hex(), "class": CLASS_NAMES[x["class"]], "basis": x["payer"].hex(), "objects": x["objects"],
"bytes": x["bytes"], "amount": x["amount"]} for x in s["lines"] if x["kind"] == ATTRIBUTION_LINE]
if attribution:
out["attribution"] = attribution
return out
def statement_outcome(doc, v):
try:
return verify_statement(doc, v)
except Stop as e:
return {"outcome": "refuse", "reason": e.reason, "at": e.at}
# ---------------------------------------------------------------- the corpus
def check(doc, quiet=False):
"""The failures: every case whose stated outcome is not the one this verifier reaches."""
fails = []
try:
got = {"statements": [statement_outcome(doc, v) for v in doc["statements"]],
"ranges": [range_outcome(v) for v in doc["ranges"]]}
except (Refuse, KeyError, ValueError, TypeError, IndexError, AttributeError) as e:
return ["the corpus does not read: %r" % e]
for sec in ("statements", "ranges"):
for case, out in zip(doc[sec], got[sec]):
if case.get("outcome") != out:
fails.append("%s/%s: stated %s, got %s" % (sec, case["name"], json.dumps(case.get("outcome"))[:140],
json.dumps(out)[:140]))
if not quiet:
bad = [x for x in fails if x.startswith(sec + "/")]
print(("PASS" if not bad else "FAIL") + " %s: %d cases" % (sec, len(doc[sec])))
return fails
def reasons_given(doc):
return {v["outcome"]["reason"] for sec in ("statements", "ranges") for v in doc[sec] if v["outcome"]["outcome"] == "refuse"}
def flip_hex(h, at):
c = "0" if h[at] != "0" else "1"
return h[:at] + c + h[at + 1:]
def mutations(doc):
"""Copies of the corpus, each with one defect a conforming verifier catches."""
def named(d, sec, name):
return next(c for c in d[sec] if c["name"] == name)
def m(label, f):
d = copy.deepcopy(doc)
f(d)
return label, d
def pool(d, sec, name):
return next(c for c in d[sec] if c["name"] == name)
def outcome(sec, name, key, value):
def f(d):
named(d, sec, name)["outcome"][key] = value
return f
def statement_byte(name, at):
def f(d):
x = named(d, "statements", name)
x["statement"] = flip_hex(x["statement"], at)
return f
yield m("an accepted statement's signature", statement_byte("relay_paid_by_edge_and_viewer", -6))
yield m("an accepted statement's amount", outcome("statements", "relay_paid_by_edge_and_viewer", "amount", 81))
yield m("an accepted statement's coverage", outcome("statements", "relay_unmetered_hop", "coverage", "whole"))
yield m("a line's free flag", lambda d: named(d, "statements", "edge_fetch_granted_and_free")["outcome"]["lines"][0].update(free=True))
yield m("a refused statement stated accepted", lambda d: named(d, "statements", "bills_bytes_not_acknowledged").update(
outcome={"outcome": "accept"}))
yield m("a refusal's place", outcome("statements", "chain_broken", "at", ["record", 0]))
yield m("a refusal's reason", outcome("statements", "sums_joules_across_classes", "reason", "statement_mismatch"))
yield m("a record's item list", lambda d: pool(d, "records", "relay_from_edge_1")["items"].__setitem__(
0, flip_hex(pool(d, "records", "relay_from_edge_1")["items"][0], 70)))
yield m("a record's signature", lambda d: pool(d, "records", "edge_from_viewer_a").update(
record=flip_hex(pool(d, "records", "edge_from_viewer_a")["record"], len(pool(d, "records", "edge_from_viewer_a")["record"]) - 3)))
yield m("a proof withdrawn from the pool", lambda d: d["proofs"].remove(pool(d, "proofs", "viewer_a")))
yield m("a hop record's figure", lambda d: pool(d, "hops", "h2").update(hop=flip_hex(pool(d, "hops", "h2")["hop"], 2 * 85)))
yield m("a vector's rooms", lambda d: named(d, "statements", "origin_fills_billed_to_grant").update(rooms=[]))
yield m("a vector's withheld record restored", lambda d: named(d, "statements", "acknowledgement_missing").update(withhold=[]))
yield m("a range proof's byte", lambda d: named(d, "ranges", "interior").update(
proof=flip_hex(named(d, "ranges", "interior")["proof"], len(named(d, "ranges", "interior")["proof"]) - 9)))
yield m("a range proof's stated length", outcome("ranges", "tail_establishes_length", "length_established", False))
yield m("a range proof's covered chunks", outcome("ranges", "head", "covered", [0, 2048]))
yield m("an audit's item", lambda d: named(d, "ranges", "audit_answered")["audit"].update(end=4000))
yield m("a range proof's content hash", lambda d: named(d, "ranges", "sealed_object").update(
content_hash=flip_hex(named(d, "ranges", "sealed_object")["content_hash"], 0)))
CHECKS = {
"ack-rules": "§3.6: a record's rules",
"self-ack": "§3.6: a self-acknowledgement",
"ack-signature": "§3.5: a record's signature",
"items-rules": "§3.6: an item list's rules",
"items-recompute": "§3.6: an item list recomputes",
"upstream": "§8.2 step 3.3: the record's upstream is the party",
"period": "§8.2 step 3.4: the window within the period",
"grant-window": "§6.3 step 2: the grant's window",
"grant-principal": "§6.3 step 3: the grant's principal",
"proof-checks": "§6.3 step 4: the proof",
"seek": "§6.3 step 5: the seek power",
"room-grants": "§8.2 step 2: a room's grants",
"chain-overlap": "§3.7: windows of a chain do not overlap",
"chain-prev": "§3.7: prev names the record before",
"declared": "§4.2: no byte both declared and acknowledged",
"pop-replayed": "§6.3: a proof pays for one record",
"hop-party": "§7.7: a hop record is the party's",
"hop-span": "§7.7: a hop record's span within the period",
"free-line": "§7.6 step 1: a free line bills nothing",
"bytes-covered": "§7.6 step 2: bytes no acknowledgement covers",
"classes-summed": "§7.6 step 3: a total across classes",
"figures": "§7.6 step 4: every other figure",
"bases": "§8.2 step 6: an attribution line's basis is given",
"statement-rules": "§7.5: a statement's rules",
"statement-signature": "§7.4: a statement's signature",
"range-bounds": "§5.3: a range within the object",
"range-hash": "§5.4 step 1: the proof's content hash",
"range-audit": "§5.4 step 2: an audit's item",
"range-exact": "§5.4 step 3: chaining values consumed exactly",
"range-root": "§5.4 step 3: the root is the content hash",
}
def main(corpus_dir):
fails = []
st = ED._self_test()
print(("PASS" if not st else "FAIL") + " Ed25519 strict rule self-test " + ",".join(st))
fails += ["ed25519 self-test"] if st else []
print(("PASS" if len(REASONS) == 19 else "FAIL") + " §11 read from the extension: %d reasons" % len(REASONS))
fails += [] if len(REASONS) == 19 else ["§11 does not read"]
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
fails += check(doc)
extra = sorted(set(os.listdir(corpus_dir)) - {"vectors.json", "README.md"})
print(("PASS" if not extra else "FAIL") + " no file but vectors.json and README.md")
fails += ["an extra file: %s" % n for n in extra]
missing = [r for r in REASONS + REUSED if r not in reasons_given(doc)]
print(("PASS" if not missing else "FAIL") + " every reason of §11, and every code it reuses, appears in the corpus")
fails += ["no case gives %s" % r for r in missing]
for label, d in mutations(doc):
caught = check(d, quiet=True)
print(("PASS" if caught else "FAIL") + " mutated copy fails: %s%s" % (label, " (%s)" % caught[0][:90] if caught else ""))
if not caught:
fails.append("a mutated copy verifies: %s" % label)
for name, what in CHECKS.items():
SKIP.clear()
SKIP.add(name)
caught = check(doc, quiet=True)
SKIP.clear()
print(("PASS" if caught else "FAIL") + " a verifier without %s fails the corpus%s" % (what, " (%s)" % caught[0][:70] if caught else ""))
if not caught:
fails.append("the corpus does not catch a verifier without %s" % what)
for x in fails:
print("FAIL " + x)
print(("PASS" if not fails else "FAIL") + " settlement corpus: %d statements, %d range proofs" % (len(doc["statements"]), len(doc["ranges"])))
return 1 if fails else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: settlement_verify.py <live-conformance/settlement>", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))