A price book of version 3 states each entry's declared uncertainty. A delivery-decision record of version 2 rests on one and states offer: the route's entry by its place in the book, what the delivery carries, its estimates and its energy figure's class, uncertainty and grade, which a verifier recomputes. A delivery in a verification document may carry its route's OER/2 receipt, signed under the route's name, naming the decision and the delivered objects' root. The receipt's grade is no higher than the offer's. A figure more than one declared uncertainty above the offer's states price_exceeded as its work.outcome, an open member of the receipt the hop signs, and no other does. So no document kind is added and no earlier record reads differently. route_record_verify.py recomputes the offer and holds each receipt to it. The controls of an unknown version move on to a price book of version 4 and a decision of version 3; every other vector is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1546 lines
77 KiB
Python
1546 lines
77 KiB
Python
#!/usr/bin/env python3
|
||
"""Independent verifier of the route record corpus (extensions/route-record.md).
|
||
|
||
Written from the extension's text. It shares no code with wai-rs, which generates the corpus,
|
||
and never runs it. It checks the router's Ed25519 signature with
|
||
../claim-conformance/_ed25519.py, WAI's strict rule (SPEC §7 *Signatures*) written from RFC 8032
|
||
with the standard library, and verifies the hops' OER/2 receipts with the OER/2 reference
|
||
verifier, ../oer-conformance/oer2_verify.py (which uses the `cryptography` package). Everything
|
||
else is here:
|
||
|
||
- the documents (§2, §3): read strictly, held to the subset, their forms and digests, and RFC
|
||
8785's canonical form;
|
||
- routing (§4): the candidates, their estimates, every hard limit, the key list, the ranking,
|
||
the order, the decision and the allowed attempts;
|
||
- the record (§5): its members and forms, its signature, its binding to the documents, its
|
||
candidates, order and decision recomputed, and its attempts held to the order and the
|
||
billing rules;
|
||
- the hop receipts (§6), and the outcome of §7.3;
|
||
- version 2 (§3.4): a price book that prices each operation of an endpoint and states its
|
||
failures over their basis (§3.2.1), the candidates of the request's operation, the expected
|
||
cost and time to first byte per successful job and the ranking that reads them (§4.2, §4.4),
|
||
a record's version held to its price book's, and the served hop's receipt held to the
|
||
outputs the record states, their commitment computed with ../claim-conformance/_blake3.py
|
||
(BLAKE3 written from its specification) as oer-receipts §3 *Output* defines it;
|
||
- version 3 of the record (§3.4), on a price book of version 2: what the served hop delivered
|
||
stated by its count and root, the served hop's receipt held to the root; an output's
|
||
inclusion proof (§6.1, `inclusion_proof`), which the receipt ledger's verifier checks a job
|
||
source's outputs with; and each third-party hop record (§6.2): its members and forms, no key,
|
||
header or URL query in it, its signature under its own domain, and its binding to its attempt,
|
||
its candidate and what its hop's receipt names;
|
||
- each delivery-decision record (§6.3): its members and forms, its signature under its own
|
||
domain, its binding to its delivery's documents, the routing of §4 over them recomputed, the
|
||
route and the key each other route was passed over by, its parent and its chain;
|
||
- version 3 of the price book (§3.4), and a delivery's offer and its route's receipt (§6.4): an
|
||
entry's declared uncertainty, the offer of a decision of version 2 recomputed from the route's
|
||
entry, and the receipt held to its route, its decision, its objects and its offer: its grade,
|
||
and its figure read to whole microjoules against the offer's and one declared uncertainty, or
|
||
the hop's flag.
|
||
|
||
For every vector it requires the outcome the corpus states; of the corpus, that every reason of
|
||
§8 and every code it reuses appears in it, that §8 names every reason it raises, and that no
|
||
other file is there. Then it mutates copies of the corpus, one defect each, and requires every
|
||
copy to fail; and it drops each of its own checks in turn and requires the corpus to catch the
|
||
weakened verifier.
|
||
|
||
python3 -I route_record_verify.py <live-conformance/routes>
|
||
python3 -I route_record_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.
|
||
"""
|
||
|
||
import copy
|
||
from fractions import Fraction
|
||
import hashlib
|
||
import io
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
|
||
sys.path.insert(0, os.path.join(HERE, "..", "oer-conformance"))
|
||
import _ed25519 as ED # noqa: E402
|
||
import oer2_verify as OER # noqa: E402
|
||
from _blake3 import blake3 # noqa: E402
|
||
|
||
EXTENSION = os.path.join(HERE, "..", "extensions", "route-record.md")
|
||
|
||
SIGN_DOMAIN = b"wai:route-record\x01" # §5.3
|
||
LIMITS = ["policy", "unit", "region", "residency", "training", "retention", "ga", "consent", "unit_price", "spend"] # §4.3
|
||
KEYS = ["price", "ttfb", "energy", "quality"] # §3.3
|
||
ERRORS = ["timeout", "server_error", "rate_limited", "refused", "unavailable"] # §5.2
|
||
CLASSES = ["CalibratedInstrument", "OnChipCounter", "ModelBased", "Estimator"] # §2
|
||
DECIDED_BY = ["none", "only_survivor", "targets", "price", "ttfb", "energy", "quality", "endpoint"] # §4.5
|
||
MAX = 2 ** 53 - 1
|
||
ENTRY = ["endpoint", "provider", "capability", "region", "residency", "trains_on_inputs", "retention_days", "ga",
|
||
"needs_consent", "unit", "divisor", "price", "ttfb_ms", "energy_uj", "energy_class", "quality"] # §3.2
|
||
ENTRY_2 = ENTRY + ["operation", "failures"] # §3.2, version 2
|
||
ENTRY_3 = ENTRY_2 + ["energy_ppm"] # §3.2, version 3
|
||
DECLARING = ["CalibratedInstrument", "OnChipCounter"] # energy-measurement §2.3
|
||
MAX_PPM = 2 ** 32 - 1 # §3.2
|
||
FAILURES = ["hops", "from", "to", "failed", "billed_price", "failed_ms"] # §3.2.1
|
||
MIN_BASIS = 100 # §3.2.1
|
||
RECORD = ["kind", "version", "job", "operation", "capability", "fallback", "quantities", "request", "policy", "pricebook",
|
||
"candidates", "order", "decision", "accepted", "deadline", "attempts", "signer", "sig"] # §5.1
|
||
ATTEMPT = ["hop", "endpoint", "outcome", "error", "start", "end", "billed", "not_billed", "receipt"] # §5.2
|
||
LEAF, NODE = b"moq-jwp:merkle-leaf\x01", b"moq-jwp:merkle-node\x01" # jwp-receipts §2.2
|
||
|
||
|
||
def book_of(version):
|
||
"""§3.4: the price book version a record of `version` rests on: 1, or 2 for versions 2 and 3."""
|
||
return 2 if version in (2, 3) else 1
|
||
|
||
|
||
def entry_members(version):
|
||
"""§3.2: an entry's members in a price book of `version`."""
|
||
return {1: ENTRY, 2: ENTRY_2, 3: ENTRY_3}[version]
|
||
|
||
|
||
def candidate_members(version):
|
||
"""§5.1: an entry's members, the five a candidate adds, and from version 2 the expected two."""
|
||
return entry_members(version) + ["via_fallback", "cost", "energy", "removed", "rank"] \
|
||
+ (["expected_cost", "expected_ttfb_ms"] if version >= 2 else [])
|
||
|
||
SKIP = set()
|
||
|
||
|
||
class Refuse(Exception):
|
||
def __init__(self, reason, at):
|
||
super().__init__(reason)
|
||
self.reason, self.at = reason, at
|
||
|
||
|
||
def on(check):
|
||
return check not in SKIP
|
||
|
||
|
||
# ---------------------------------------------------------------- §2 encodings
|
||
|
||
class _Bad(ValueError):
|
||
pass
|
||
|
||
|
||
def _no_duplicates(pairs):
|
||
d = {}
|
||
for k, v in pairs:
|
||
if k in d:
|
||
raise _Bad("member %r twice" % k)
|
||
d[k] = v
|
||
return d
|
||
|
||
|
||
def _int(s):
|
||
# A number is an integer 0 to 2^53 - 1 in plain decimal: no sign (so `-0` too is out).
|
||
if s.startswith("-"):
|
||
raise _Bad("a negative number")
|
||
v = int(s)
|
||
if v > MAX:
|
||
raise _Bad("an integer past 2^53 - 1")
|
||
return v
|
||
|
||
|
||
def _not_a_number(s):
|
||
raise _Bad("not an integer: %s" % s)
|
||
|
||
|
||
def _walk(v):
|
||
stack = [(v, 1)]
|
||
while stack:
|
||
x, d = stack.pop()
|
||
yield x, d
|
||
if isinstance(x, dict):
|
||
for k, y in x.items():
|
||
stack.append((k, d + 1))
|
||
stack.append((y, d + 1))
|
||
elif isinstance(x, list):
|
||
stack.extend((y, d + 1) for y in x)
|
||
|
||
|
||
def read_document(text):
|
||
"""§2: a document, or None (`malformed`)."""
|
||
if not isinstance(text, str):
|
||
return None
|
||
try:
|
||
v = json.loads(text, object_pairs_hook=_no_duplicates, parse_int=_int, parse_float=_not_a_number,
|
||
parse_constant=_not_a_number)
|
||
except (ValueError, RecursionError):
|
||
return None
|
||
depth = 0
|
||
for x, d in _walk(v):
|
||
if isinstance(x, (dict, list)):
|
||
depth = max(depth, d)
|
||
if isinstance(x, str) and any("\ud800" <= c <= "\udfff" for c in x):
|
||
return None
|
||
if depth > 64:
|
||
return None
|
||
return v
|
||
|
||
|
||
def canonical(v):
|
||
"""RFC 8785 over §2's subset: members sorted by their UTF-16 code units, strings escaped as
|
||
§3.2.2.2 there, integers in plain decimal."""
|
||
if v is None:
|
||
return "null"
|
||
if v is True:
|
||
return "true"
|
||
if v is False:
|
||
return "false"
|
||
if isinstance(v, int):
|
||
return str(v)
|
||
if isinstance(v, str):
|
||
return json.dumps(v, ensure_ascii=False)
|
||
if isinstance(v, list):
|
||
return "[" + ",".join(canonical(x) for x in v) + "]"
|
||
keys = sorted(v, key=lambda k: k.encode("utf-16-be"))
|
||
return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + canonical(v[k]) for k in keys) + "}"
|
||
|
||
|
||
def digest(v):
|
||
return hashlib.sha256(canonical(v).encode("utf-8")).hexdigest()
|
||
|
||
|
||
def same(a, b):
|
||
"""Two values are the same JSON value: compared by their canonical forms, so `0` is not
|
||
`false` and `1` not `true`, as Python's `==` would have them."""
|
||
return canonical(a) == canonical(b)
|
||
|
||
|
||
TOKEN = re.compile(r"\A[A-Za-z0-9._:/-]{1,128}\Z")
|
||
UNIT = re.compile(r"\A[a-z][a-z0-9_]{0,63}\Z")
|
||
CAP = re.compile(r"\Awai\.[a-z0-9_.]{0,124}\Z")
|
||
OPERATION = re.compile(r"\A[a-z][a-z0-9]*\.[a-z][a-z0-9]*\Z")
|
||
CURRENCY = re.compile(r"\A[A-Z]{3}\Z")
|
||
RECEIPT_ID = re.compile(r"\A(?:sha256|blake3):[0-9a-f]{64}\Z")
|
||
|
||
|
||
def is_int(v, lo=0, hi=MAX):
|
||
return isinstance(v, int) and not isinstance(v, bool) and lo <= v <= hi
|
||
|
||
|
||
def is_token(v):
|
||
return isinstance(v, str) and TOKEN.match(v) is not None
|
||
|
||
|
||
def is_unit(v):
|
||
return isinstance(v, str) and UNIT.match(v) is not None
|
||
|
||
|
||
def is_cap(v):
|
||
return isinstance(v, str) and CAP.match(v) is not None
|
||
|
||
|
||
def is_hex(v, n):
|
||
return isinstance(v, str) and len(v) == n and all(c in "0123456789abcdef" for c in v)
|
||
|
||
|
||
def exactly(v, members):
|
||
return isinstance(v, dict) and set(v) == set(members) and len(v) == len(members)
|
||
|
||
|
||
def ascending(xs):
|
||
return all(xs[i].encode() < xs[i + 1].encode() for i in range(len(xs) - 1))
|
||
|
||
|
||
def null_or(v, f):
|
||
return v is None or f(v)
|
||
|
||
|
||
def tokens(v, top):
|
||
return isinstance(v, list) and 1 <= len(v) <= top and all(is_token(x) for x in v) and ascending(v)
|
||
|
||
|
||
def quantities_ok(v):
|
||
if not (isinstance(v, list) and 1 <= len(v) <= 16):
|
||
return False
|
||
if not all(exactly(q, ["unit", "quantity"]) and is_unit(q["unit"]) and is_int(q["quantity"], 1) for q in v):
|
||
return False
|
||
return ascending([q["unit"] for q in v])
|
||
|
||
|
||
# ---------------------------------------------------------------- §3 the documents
|
||
|
||
def request_ok(r):
|
||
if not exactly(r, ["kind", "job", "operation", "capability", "fallback", "quantities", "limits", "prefer", "deadline_us"]):
|
||
return False
|
||
if r["kind"] != "wai-route-request" or not is_token(r["job"]) or not isinstance(r["operation"], str) \
|
||
or not OPERATION.match(r["operation"]) or not is_cap(r["capability"]):
|
||
return False
|
||
if not null_or(r["fallback"], lambda f: is_cap(f) and f != r["capability"]):
|
||
return False
|
||
if not quantities_ok(r["quantities"]):
|
||
return False
|
||
lim = r["limits"]
|
||
if not exactly(lim, ["regions", "residency", "no_training", "zero_retention", "ga_only", "consent_on_file", "max_price", "spend_ceiling"]):
|
||
return False
|
||
if not all(null_or(lim[k], lambda x: tokens(x, 64)) for k in ("regions", "residency")):
|
||
return False
|
||
if not all(isinstance(lim[k], bool) for k in ("no_training", "zero_retention", "ga_only", "consent_on_file")):
|
||
return False
|
||
mp = lim["max_price"]
|
||
if not (isinstance(mp, list) and len(mp) <= 16 and all(
|
||
exactly(m, ["unit", "price", "divisor"]) and is_unit(m["unit"]) and is_int(m["price"]) and is_int(m["divisor"], 1)
|
||
for m in mp) and ascending([m["unit"] for m in mp])):
|
||
return False
|
||
if not null_or(lim["spend_ceiling"], lambda c: exactly(c, ["currency", "amount"]) and isinstance(c["currency"], str)
|
||
and CURRENCY.match(c["currency"]) is not None and is_int(c["amount"])):
|
||
return False
|
||
p = r["prefer"]
|
||
if not exactly(p, ["sort", "ttfb_ms", "quality"]):
|
||
return False
|
||
if not (null_or(p["sort"], lambda s: s in KEYS) and null_or(p["ttfb_ms"], is_int)
|
||
and null_or(p["quality"], lambda q: is_int(q, 0, 1000))):
|
||
return False
|
||
return null_or(r["deadline_us"], lambda d: is_int(d, 1))
|
||
|
||
|
||
def failures_form(f):
|
||
"""§3.2.1's table, each member of its form."""
|
||
return exactly(f, FAILURES) and is_int(f["hops"], 1) and all(is_int(f[k]) for k in FAILURES)
|
||
|
||
|
||
def failures_ok(f, issued):
|
||
"""§3.2.1: the form, and its relations: no more failed than sent, a period that ends by the
|
||
book's issue, and no billed price or time without a failed hop."""
|
||
if not failures_form(f):
|
||
return False
|
||
if not on("failures-relations"):
|
||
return True
|
||
return f["failed"] <= f["hops"] and f["from"] <= f["to"] <= issued \
|
||
and (f["failed"] > 0 or (f["billed_price"] == 0 and f["failed_ms"] == 0))
|
||
|
||
|
||
def ppm_ok(v):
|
||
"""§3.2: `energy_ppm`'s form."""
|
||
return v is None or is_int(v, 1, MAX_PPM)
|
||
|
||
|
||
def entry_ok(e, version=1, issued=0):
|
||
if not exactly(e, entry_members(version)):
|
||
return False
|
||
if version >= 2 and not (isinstance(e["operation"], str) and OPERATION.match(e["operation"])
|
||
and null_or(e["failures"], lambda f: failures_ok(f, issued))):
|
||
return False
|
||
if version == 3 and on("ppm-form") and not ppm_ok(e["energy_ppm"]):
|
||
return False
|
||
if version == 3 and on("ppm-class") and (e["energy_ppm"] is not None) != (e["energy_class"] in DECLARING):
|
||
return False
|
||
return (is_token(e["endpoint"]) and is_token(e["provider"]) and is_cap(e["capability"])
|
||
and null_or(e["region"], is_token) and null_or(e["residency"], is_token)
|
||
and null_or(e["trains_on_inputs"], lambda b: isinstance(b, bool)) and null_or(e["retention_days"], is_int)
|
||
and null_or(e["ga"], lambda b: isinstance(b, bool)) and null_or(e["needs_consent"], lambda b: isinstance(b, bool))
|
||
and is_unit(e["unit"]) and is_int(e["divisor"], 1) and is_int(e["price"]) and null_or(e["ttfb_ms"], is_int)
|
||
and null_or(e["energy_uj"], is_int) and null_or(e["energy_class"], lambda c: c in CLASSES)
|
||
and (e["energy_uj"] is None) == (e["energy_class"] is None)
|
||
and null_or(e["quality"], lambda q: is_int(q, 0, 1000)))
|
||
|
||
|
||
def book_version(b):
|
||
"""§3.4: a price book with no `version` member is of version 1."""
|
||
return b.get("version", 1) if isinstance(b, dict) else 1
|
||
|
||
|
||
def pricebook_ok(b):
|
||
if not isinstance(b, dict):
|
||
return False
|
||
# A `version` member gives a later version's members; its value must be 2 or 3.
|
||
known = is_int(b.get("version")) and b["version"] in (2, 3)
|
||
shape = (b["version"] if known else 2) if "version" in b else 1
|
||
if shape >= 2 and on("pricebook-version") and not known:
|
||
return False
|
||
members = ["kind", "version", "currency", "issued", "entries"] if shape >= 2 else ["kind", "currency", "issued", "entries"]
|
||
if not exactly(b, members) or b["kind"] != "wai-route-pricebook":
|
||
return False
|
||
if not (isinstance(b["currency"], str) and CURRENCY.match(b["currency"]) and is_int(b["issued"])):
|
||
return False
|
||
es = b["entries"]
|
||
if not (isinstance(es, list) and 1 <= len(es) <= 1024 and all(entry_ok(e, shape, b["issued"]) for e in es)):
|
||
return False
|
||
if shape >= 2:
|
||
# By endpoint, then operation: compared as a pair of byte strings, never joined.
|
||
keys = [(e["endpoint"].encode(), e["operation"].encode()) for e in es]
|
||
return all(keys[i] < keys[i + 1] for i in range(len(keys) - 1))
|
||
return ascending([e["endpoint"] for e in es])
|
||
|
||
|
||
def policy_ok(p):
|
||
if not exactly(p, ["kind", "name", "version", "keys", "max_attempts", "exclude"]) or p["kind"] != "wai-route-policy":
|
||
return False
|
||
if not (is_token(p["name"]) and is_int(p["version"], 1) and is_int(p["max_attempts"], 1, 16)):
|
||
return False
|
||
if not (isinstance(p["keys"], list) and len(p["keys"]) == 4 and sorted(k for k in p["keys"] if isinstance(k, str)) == sorted(KEYS)):
|
||
return False
|
||
ex = p["exclude"]
|
||
return isinstance(ex, list) and len(ex) <= 64 and all(is_token(x) for x in ex) and ascending(ex)
|
||
|
||
|
||
def read(text, ok, invalid, at):
|
||
v = read_document(text)
|
||
if v is None:
|
||
raise Refuse("malformed", at)
|
||
if on(invalid) and not ok(v):
|
||
raise Refuse(invalid, at)
|
||
return v
|
||
|
||
|
||
# ---------------------------------------------------------------- §4 routing
|
||
|
||
def quantity(req, unit):
|
||
for q in req["quantities"]:
|
||
if q["unit"] == unit:
|
||
return q["quantity"]
|
||
return None
|
||
|
||
|
||
def estimate(q, x, d):
|
||
"""§4.2: ⌊q × x / d⌋, null past 2^53 - 1."""
|
||
if q is None or x is None:
|
||
return None
|
||
e = q * x // d
|
||
return e if e <= MAX else None
|
||
|
||
|
||
def removals(req, book, pol, e, cost):
|
||
"""§4.3, in its order: (limit, value)."""
|
||
lim, out = req["limits"], []
|
||
if on("limit-policy") and e["provider"] in pol["exclude"]:
|
||
out.append(("policy", e["provider"]))
|
||
if on("limit-unit") and quantity(req, e["unit"]) is None:
|
||
out.append(("unit", e["unit"]))
|
||
for name, key in (("region", "regions"), ("residency", "residency")):
|
||
allowed = lim[key]
|
||
if on("limit-" + name) and allowed is not None and (e[name] is None or e[name] not in allowed):
|
||
out.append((name, e[name]))
|
||
if on("limit-training") and lim["no_training"] and e["trains_on_inputs"] is not False:
|
||
out.append(("training", e["trains_on_inputs"]))
|
||
if on("limit-retention") and lim["zero_retention"] and e["retention_days"] != 0:
|
||
out.append(("retention", e["retention_days"]))
|
||
if on("limit-ga") and lim["ga_only"] and e["ga"] is not True:
|
||
out.append(("ga", e["ga"]))
|
||
if on("limit-consent") and not lim["consent_on_file"] and e["needs_consent"] is not False:
|
||
out.append(("consent", e["needs_consent"]))
|
||
for m in lim["max_price"]:
|
||
if on("limit-unit-price") and m["unit"] == e["unit"] and e["price"] * m["divisor"] > m["price"] * e["divisor"]:
|
||
out.append(("unit_price", e["price"]))
|
||
c = lim["spend_ceiling"]
|
||
if on("limit-spend") and c is not None and (c["currency"] != book["currency"] or cost is None or cost > c["amount"]):
|
||
out.append(("spend", cost))
|
||
return out
|
||
|
||
|
||
def expected(q, e, cost):
|
||
"""§4.2, version 2: the expected cost and time to first byte per successful job. A job sent
|
||
to the endpoint until served meets failed / (hops - failed) failures on average; each costs
|
||
the router its billed price for the job's quantity and costs the job its time. Unknown
|
||
failures (none stated, or fewer than 100 hops), an unknown list figure, a basis every hop
|
||
of which failed, and a figure past 2^53 - 1 give None."""
|
||
f = e["failures"]
|
||
least = MIN_BASIS if on("thin-basis") else 1
|
||
if f is None or f["hops"] < least:
|
||
return None, None
|
||
ok = f["hops"] - f["failed"]
|
||
if ok == 0:
|
||
if on("unbounded"):
|
||
return None, None
|
||
ok = None
|
||
|
||
def plus(lst, per_failure):
|
||
if lst is None or per_failure is None:
|
||
return None
|
||
t = lst + (0 if ok is None else per_failure * f["failed"] // ok)
|
||
return t if t <= MAX else None
|
||
return plus(cost, estimate(q, f["billed_price"], e["divisor"])), plus(e["ttfb_ms"], f["failed_ms"])
|
||
|
||
|
||
def ttfb_read(c):
|
||
"""The time to first byte §4.4 reads: in version 2, the expected one."""
|
||
return c["expected_ttfb_ms"] if "expected_ttfb_ms" in c and on("expected-targets") else c["ttfb_ms"]
|
||
|
||
|
||
def meets(req, c):
|
||
t, q = req["prefer"]["ttfb_ms"], req["prefer"]["quality"]
|
||
return (t is None or (ttfb_read(c) is not None and ttfb_read(c) <= t)) and \
|
||
(q is None or (c["quality"] is not None and c["quality"] >= q))
|
||
|
||
|
||
def key_list(req, pol):
|
||
"""§4.4."""
|
||
p, k = req["prefer"], []
|
||
if p["ttfb_ms"] is not None or p["quality"] is not None:
|
||
k.append("targets")
|
||
if p["sort"] is not None:
|
||
k.append(p["sort"])
|
||
k += [x for x in pol["keys"] if x != p["sort"]]
|
||
return k + ["endpoint"]
|
||
|
||
|
||
def compare(req, key, a, b):
|
||
"""-1 when a comes first under the key, 1 when b does, 0 when they do not differ."""
|
||
def nulls_last(x, y, high_first=False):
|
||
if x is None and y is None:
|
||
return 0
|
||
if x is None:
|
||
return 1
|
||
if y is None:
|
||
return -1
|
||
if x == y:
|
||
return 0
|
||
return (-1 if x > y else 1) if high_first else (-1 if x < y else 1)
|
||
v2 = "expected_cost" in a
|
||
if key == "targets":
|
||
ma, mb = meets(req, a), meets(req, b)
|
||
return 0 if ma == mb else (-1 if ma else 1)
|
||
if key == "price":
|
||
# Version 2: the expected cost, then the list cost (§4.4).
|
||
first = nulls_last(a["expected_cost"], b["expected_cost"]) if v2 and on("expected-price") else 0
|
||
return first or nulls_last(a["cost"], b["cost"])
|
||
if key == "ttfb":
|
||
first = nulls_last(a["expected_ttfb_ms"], b["expected_ttfb_ms"]) if v2 and on("expected-ttfb") else 0
|
||
return first or nulls_last(a["ttfb_ms"], b["ttfb_ms"])
|
||
if key == "energy":
|
||
return nulls_last(a["energy"], b["energy"])
|
||
if key == "quality":
|
||
return nulls_last(a["quality"], b["quality"], high_first=True)
|
||
ea, eb = a["endpoint"].encode(), b["endpoint"].encode()
|
||
return 0 if ea == eb else (-1 if ea < eb else 1)
|
||
|
||
|
||
def route(req, book, pol):
|
||
"""§4: the candidates as the record states them, the order, the decision, the allowed."""
|
||
cands = []
|
||
v2 = book_version(book) != 1
|
||
for e in book["entries"]:
|
||
if v2 and on("operation-candidates") and e["operation"] != req["operation"]:
|
||
continue
|
||
if e["capability"] == req["capability"]:
|
||
via = False
|
||
elif req["fallback"] is not None and e["capability"] == req["fallback"]:
|
||
via = True
|
||
else:
|
||
continue
|
||
q = quantity(req, e["unit"])
|
||
cost = estimate(q, e["price"], e["divisor"])
|
||
energy = estimate(q, e["energy_uj"], e["divisor"])
|
||
c = dict(e)
|
||
c.update(via_fallback=via, cost=cost, energy=energy,
|
||
removed=[{"limit": l, "value": v} for l, v in removals(req, book, pol, e, cost)], rank=None)
|
||
if v2:
|
||
c["expected_cost"], c["expected_ttfb_ms"] = expected(q, e, cost)
|
||
cands.append(c)
|
||
keys = key_list(req, pol)
|
||
survivors = [c for c in cands if not c["removed"]]
|
||
|
||
def first(a, b):
|
||
for k in keys:
|
||
r = compare(req, k, a, b)
|
||
if r:
|
||
return r, k
|
||
return 0, None
|
||
# An insertion sort under the total order of the key list.
|
||
order = []
|
||
for c in survivors:
|
||
i = len(order)
|
||
while i > 0 and first(c, order[i - 1])[0] < 0:
|
||
i -= 1
|
||
order.insert(i, c)
|
||
for n, c in enumerate(order):
|
||
c["rank"] = n + 1
|
||
if not order:
|
||
decision = {"decided_by": "none", "runner_up": None}
|
||
elif len(order) == 1:
|
||
decision = {"decided_by": "only_survivor", "runner_up": None}
|
||
else:
|
||
decision = {"decided_by": first(order[0], order[1])[1], "runner_up": order[1]["endpoint"]}
|
||
allowed = min(len(order), pol["max_attempts"])
|
||
return cands, [c["endpoint"] for c in order], decision, allowed
|
||
|
||
|
||
# ---------------------------------------------------------------- §5 the record
|
||
|
||
def scalar(v):
|
||
return not isinstance(v, (dict, list))
|
||
|
||
|
||
def candidate_form(c, version=1):
|
||
"""§5.1: each member of its own form. Relations between members (an energy figure and its
|
||
class, a cost and its price, a failure count and its hops) are the recomputation's, at §7.2
|
||
step 6."""
|
||
if not exactly(c, candidate_members(version)):
|
||
return False
|
||
if version >= 2 and not (isinstance(c["operation"], str) and OPERATION.match(c["operation"])
|
||
and null_or(c["failures"], failures_form)
|
||
and null_or(c["expected_cost"], is_int) and null_or(c["expected_ttfb_ms"], is_int)):
|
||
return False
|
||
if version == 3 and not ppm_ok(c["energy_ppm"]):
|
||
return False
|
||
is_bool = lambda b: isinstance(b, bool) # noqa: E731
|
||
return (is_token(c["endpoint"]) and is_token(c["provider"]) and is_cap(c["capability"])
|
||
and null_or(c["region"], is_token) and null_or(c["residency"], is_token)
|
||
and null_or(c["trains_on_inputs"], is_bool) and null_or(c["retention_days"], is_int)
|
||
and null_or(c["ga"], is_bool) and null_or(c["needs_consent"], is_bool)
|
||
and is_unit(c["unit"]) and is_int(c["divisor"], 1) and is_int(c["price"]) and null_or(c["ttfb_ms"], is_int)
|
||
and null_or(c["energy_uj"], is_int) and null_or(c["energy_class"], lambda x: x in CLASSES)
|
||
and null_or(c["quality"], lambda q: is_int(q, 0, 1000))
|
||
and isinstance(c["via_fallback"], bool) and null_or(c["cost"], is_int) and null_or(c["energy"], is_int)
|
||
and isinstance(c["removed"], list) and len(c["removed"]) <= len(LIMITS)
|
||
and all(exactly(x, ["limit", "value"]) and x["limit"] in LIMITS and scalar(x["value"]) for x in c["removed"])
|
||
and null_or(c["rank"], lambda n: is_int(n, 1)))
|
||
|
||
|
||
def outputs_form(a):
|
||
"""§5.2, version 2: a served attempt delivered 1 to 1024 objects, each named once by its
|
||
content hash; a failed one, none."""
|
||
xs = a["outputs"]
|
||
if not (isinstance(xs, list) and all(is_hex(x, 64) for x in xs) and len(set(xs)) == len(xs)):
|
||
return False
|
||
return 1 <= len(xs) <= 1024 if a["outcome"] == "served" else not xs
|
||
|
||
|
||
def delivered_form(a):
|
||
"""§5.2, version 3: a served attempt's count, at least 1, and the root of what it delivered,
|
||
64 lowercase hexadecimal digits; a failed one's, 0 and 64 zeros."""
|
||
d = a["delivered"]
|
||
if not (exactly(d, ["count", "root"]) and is_int(d["count"]) and is_hex(d["root"], 64)):
|
||
return False
|
||
return d["count"] >= 1 if a["outcome"] == "served" else d["count"] == 0 and d["root"] == "00" * 32
|
||
|
||
|
||
def attempt_form(i, a, version=1):
|
||
extra = {2: ["outputs"], 3: ["delivered"]}.get(version, [])
|
||
if not exactly(a, ATTEMPT + extra) or a["outcome"] not in ("served", "failed"):
|
||
return False
|
||
if version == 2 and on("outputs-form") and not outputs_form(a):
|
||
return False
|
||
if version == 3 and on("delivered-form") and not delivered_form(a):
|
||
return False
|
||
served = a["outcome"] == "served"
|
||
error_ok = a["error"] is None if served else a["error"] in ERRORS
|
||
return (is_int(a["hop"]) and a["hop"] == i + 1 and is_token(a["endpoint"]) and error_ok and is_int(a["start"])
|
||
and is_int(a["end"]) and isinstance(a["billed"], bool) and a["not_billed"] in (None, "failed", "deadline_missed")
|
||
and isinstance(a["receipt"], str) and RECEIPT_ID.match(a["receipt"]) is not None)
|
||
|
||
|
||
def record_form(r):
|
||
if not exactly(r, RECORD):
|
||
return False
|
||
v = r["version"]
|
||
return (is_int(v) and v in (1, 2, 3) and is_token(r["job"]) and isinstance(r["operation"], str)
|
||
and OPERATION.match(r["operation"]) is not None and is_cap(r["capability"]) and null_or(r["fallback"], is_cap)
|
||
and quantities_ok(r["quantities"]) and is_hex(r["request"], 64)
|
||
and exactly(r["policy"], ["name", "version", "digest"]) and is_token(r["policy"]["name"])
|
||
and is_int(r["policy"]["version"], 1) and is_hex(r["policy"]["digest"], 64)
|
||
and exactly(r["pricebook"], ["digest", "currency", "issued"]) and is_hex(r["pricebook"]["digest"], 64)
|
||
and isinstance(r["pricebook"]["currency"], str) and CURRENCY.match(r["pricebook"]["currency"]) is not None
|
||
and is_int(r["pricebook"]["issued"])
|
||
and isinstance(r["candidates"], list) and len(r["candidates"]) <= 1024 and all(candidate_form(c, book_of(v)) for c in r["candidates"])
|
||
and isinstance(r["order"], list) and len(r["order"]) <= 1024 and all(is_token(x) for x in r["order"])
|
||
and exactly(r["decision"], ["decided_by", "runner_up"]) and r["decision"]["decided_by"] in DECIDED_BY
|
||
and null_or(r["decision"]["runner_up"], is_token)
|
||
and is_int(r["accepted"]) and null_or(r["deadline"], is_int)
|
||
and isinstance(r["attempts"], list) and len(r["attempts"]) <= 16
|
||
and all(attempt_form(i, a, v) for i, a in enumerate(r["attempts"]))
|
||
and is_hex(r["signer"], 64) and is_hex(r["sig"], 128))
|
||
|
||
|
||
_VERIFIED = {}
|
||
|
||
|
||
def ed_verify(key, msg, sig):
|
||
k = (key, hashlib.sha256(msg).digest(), sig)
|
||
if k not in _VERIFIED:
|
||
_VERIFIED[k] = ED.verify(key, msg, sig)
|
||
return _VERIFIED[k]
|
||
|
||
|
||
def signing_bytes(r):
|
||
body = {k: v for k, v in r.items() if k != "sig"}
|
||
return SIGN_DOMAIN + canonical(body).encode("utf-8")
|
||
|
||
|
||
def billing(served, end, deadline):
|
||
"""§5.2: (billed, not_billed)."""
|
||
if not served:
|
||
return False, "failed"
|
||
if deadline is not None and end > deadline:
|
||
return False, "deadline_missed"
|
||
return True, None
|
||
|
||
|
||
# ---------------------------------------------------------------- §7 verification
|
||
|
||
def verify(doc):
|
||
try:
|
||
return verify_steps(doc)
|
||
except Refuse as f:
|
||
return {"outcome": "refuse", "reason": f.reason, "at": f.at}
|
||
|
||
|
||
def verify_steps(doc):
|
||
has_third = isinstance(doc, dict) and "third_party" in doc
|
||
has_deliveries = isinstance(doc, dict) and "deliveries" in doc
|
||
members = ["request", "pricebook", "policy", "record", "receipts", "keys"] + (["third_party"] if has_third else []) \
|
||
+ (["deliveries"] if has_deliveries else [])
|
||
if not exactly(doc, members) \
|
||
or (has_deliveries and not (isinstance(doc["deliveries"], list) and all(
|
||
exactly(x, ["request", "pricebook", "policy", "decision"] + (["receipt"] if isinstance(x, dict) and "receipt" in x else []))
|
||
and all(isinstance(x[k], str) for k in x)
|
||
for x in doc["deliveries"]))) \
|
||
or (has_deliveries and doc["keys"] is None and any("receipt" in x for x in doc["deliveries"])) \
|
||
or (has_third and not (isinstance(doc["third_party"], list) and all(isinstance(x, str) for x in doc["third_party"]))) \
|
||
or (has_third and doc["receipts"] is None) \
|
||
or not all(isinstance(doc[k], str) for k in ("request", "pricebook", "policy", "record")) \
|
||
or not (doc["receipts"] is None or (isinstance(doc["receipts"], list) and all(isinstance(x, str) for x in doc["receipts"]))) \
|
||
or not (doc["keys"] is None or isinstance(doc["keys"], dict)) \
|
||
or (doc["receipts"] is not None and doc["keys"] is None):
|
||
raise Refuse("malformed", [])
|
||
# 1-3. The documents.
|
||
req = read(doc["request"], request_ok, "request_invalid", ["request"])
|
||
book = read(doc["pricebook"], pricebook_ok, "pricebook_invalid", ["pricebook"])
|
||
pol = read(doc["policy"], policy_ok, "policy_invalid", ["policy"])
|
||
# 4. The record.
|
||
rec = read_document(doc["record"])
|
||
if not isinstance(rec, dict) or rec.get("kind") != "wai-route-record":
|
||
raise Refuse("malformed", ["record"])
|
||
if on("version") and is_int(rec.get("version")) and rec["version"] not in (1, 2, 3):
|
||
raise Refuse("unknown_version", ["record", "version"])
|
||
if on("record-form") and not record_form(rec):
|
||
raise Refuse("record_invalid", ["record"])
|
||
if on("signature"):
|
||
bad = ed_verify(bytes.fromhex(rec["signer"]), signing_bytes(rec), bytes.fromhex(rec["sig"]))
|
||
if bad:
|
||
raise Refuse(bad, ["record", "sig"])
|
||
# 5. The binding.
|
||
deadline = None if req["deadline_us"] is None else rec["accepted"] + req["deadline_us"]
|
||
if on("binding-request"):
|
||
for m, want in (("job", req["job"]), ("operation", req["operation"]), ("capability", req["capability"]),
|
||
("fallback", req["fallback"]), ("quantities", req["quantities"]), ("request", digest(req)),
|
||
("deadline", deadline)):
|
||
if not same(rec[m], want):
|
||
raise Refuse("request_mismatch", ["record", m])
|
||
if on("binding-policy") and not same(rec["policy"], {"name": pol["name"], "version": pol["version"], "digest": digest(pol)}):
|
||
raise Refuse("policy_mismatch", ["record", "policy"])
|
||
if on("record-version") and not book_of(rec["version"]) == book_version(book):
|
||
raise Refuse("pricebook_mismatch", ["record", "version"])
|
||
if on("binding-pricebook") and not same(rec["pricebook"], {"digest": digest(book), "currency": book["currency"], "issued": book["issued"]}):
|
||
raise Refuse("pricebook_mismatch", ["record", "pricebook"])
|
||
# 6. The candidates, the order, the decision.
|
||
cands, order, decision, allowed = route(req, book, pol)
|
||
stated = rec["candidates"]
|
||
for i in range(max(len(stated), len(cands))):
|
||
at = ["record", "candidates", i]
|
||
if i >= len(stated) or i >= len(cands):
|
||
raise Refuse("candidate_mismatch", at)
|
||
s, c = stated[i], cands[i]
|
||
if on("candidates") and not same({k: v for k, v in s.items() if k not in ("removed", "rank")},
|
||
{k: v for k, v in c.items() if k not in ("removed", "rank")}):
|
||
raise Refuse("candidate_mismatch", at)
|
||
if on("removals") and not same(s["removed"], c["removed"]):
|
||
raise Refuse("removal_mismatch", at)
|
||
if on("ranks") and not same(s["rank"], c["rank"]):
|
||
raise Refuse("rank_mismatch", at)
|
||
if on("order") and not same(rec["order"], order):
|
||
raise Refuse("rank_mismatch", ["record", "order"])
|
||
if on("decision") and not same(rec["decision"], decision):
|
||
raise Refuse("decision_mismatch", ["record", "decision"])
|
||
# 7. The attempts.
|
||
atts, prev_end = rec["attempts"], rec["accepted"]
|
||
for i, a in enumerate(atts):
|
||
at = ["record", "attempts", i]
|
||
served = a["outcome"] == "served"
|
||
if on("allowed") and i >= allowed:
|
||
raise Refuse("attempts_exceeded", at)
|
||
if on("removed") and a["endpoint"] not in order:
|
||
raise Refuse("attempt_removed", at)
|
||
if on("in-order") and (i >= len(order) or a["endpoint"] != order[i]):
|
||
raise Refuse("attempt_out_of_order", at)
|
||
if on("served-last") and served and i != len(atts) - 1:
|
||
raise Refuse("served_not_last", at)
|
||
if on("times") and (a["start"] < prev_end or a["end"] < a["start"]):
|
||
raise Refuse("times_invalid", at)
|
||
prev_end = a["end"]
|
||
billed, not_billed = billing(served, a["end"], deadline)
|
||
if on("billing") and not (same(a["billed"], billed) and same(a["not_billed"], not_billed)):
|
||
raise Refuse("billing_invalid", at)
|
||
last_served = bool(atts) and atts[-1]["outcome"] == "served"
|
||
if on("complete") and not last_served and len(atts) < allowed:
|
||
raise Refuse("attempts_incomplete", ["record", "attempts"])
|
||
# 8. The hop receipts.
|
||
unmetered = None
|
||
if doc["receipts"] is not None:
|
||
unmetered = hop_receipts(rec, doc["receipts"], doc["keys"])
|
||
# 9. The third-party hop records.
|
||
third_party = third_party_records(rec, doc["third_party"], doc["receipts"]) if has_third else None
|
||
# 10. The delivery decisions.
|
||
deliveries = delivery_decisions(rec, doc["deliveries"], doc["keys"]) if has_deliveries else None
|
||
served_attempt = atts[-1] if last_served else None
|
||
billed_hop = next((a["hop"] for a in atts if a["billed"]), None)
|
||
if deadline is None or served_attempt is None:
|
||
dl = "none"
|
||
else:
|
||
dl = "missed" if served_attempt["end"] > deadline else "met"
|
||
accepted = {
|
||
"outcome": "accept",
|
||
"digest": digest(rec),
|
||
"job": req["job"],
|
||
"operation": req["operation"],
|
||
"served": None if served_attempt is None else dict(
|
||
{"endpoint": served_attempt["endpoint"], "hop": served_attempt["hop"]},
|
||
**({"outputs": served_attempt["outputs"]} if "outputs" in served_attempt else {}),
|
||
**({"delivered": served_attempt["delivered"]} if "delivered" in served_attempt else {})),
|
||
"billed": billed_hop,
|
||
"deadline": dl,
|
||
"order": order,
|
||
"decision": decision,
|
||
"removed": [{"endpoint": c["endpoint"], "limits": [x["limit"] for x in c["removed"]]} for c in cands if c["removed"]],
|
||
"receipts": "checked" if unmetered is not None else "not_checked",
|
||
"unmetered": unmetered,
|
||
}
|
||
if third_party is not None:
|
||
accepted["third_party"] = third_party
|
||
if deliveries is not None:
|
||
accepted["deliveries"] = deliveries
|
||
return accepted
|
||
|
||
|
||
def commitment(outputs):
|
||
"""oer-receipts §3 *Output*: one output's content hash, or the jwp-receipts §2.2 root over
|
||
several in their order, every leaf unlabelled, an odd level padded with its last node."""
|
||
hs = [bytes.fromhex(x) for x in outputs]
|
||
if len(hs) == 1:
|
||
return hs[0].hex()
|
||
level = [blake3(LEAF + h) for h in hs]
|
||
while len(level) > 1:
|
||
if len(level) % 2:
|
||
level.append(level[-1])
|
||
level = [blake3(NODE + level[i] + level[i + 1]) for i in range(0, len(level), 2)]
|
||
return level[0].hex() if level else "00" * 32
|
||
|
||
|
||
def inclusion_proof(h, index, siblings, count, root):
|
||
"""§6.1: whether `siblings` proves the object whose content hash is `h` (bytes) the one at
|
||
`index` of `count` objects delivered under `root` (bytes): `index` below `count`, exactly
|
||
ceil(log2 count) siblings (none for one object, whose root is its content hash), and the walk
|
||
from its unlabelled leaf, the sibling on the left where bit i of `index` is 1, ends at
|
||
`root`."""
|
||
if not (0 <= index < count) or len(siblings) != (count - 1).bit_length():
|
||
return False
|
||
if count == 1:
|
||
return h == root
|
||
node = blake3(LEAF + h)
|
||
for i, sib in enumerate(siblings):
|
||
node = blake3(NODE + sib + node) if (index >> i) & 1 else blake3(NODE + node + sib)
|
||
return node == root
|
||
|
||
|
||
def hop_receipts(rec, texts, keys):
|
||
"""§6: each attempt held to its hop's OER/2 receipt. The hops whose receipts state the
|
||
unmetered marker."""
|
||
read_ = []
|
||
for j, t in enumerate(texts):
|
||
try:
|
||
read_.append(OER.load(io.StringIO(t)))
|
||
except ValueError:
|
||
raise Refuse("receipt_invalid", ["receipts", j])
|
||
unmetered = []
|
||
for i, a in enumerate(rec["attempts"]):
|
||
at = ["record", "attempts", i, "receipt"]
|
||
r = next((x for x in read_ if isinstance(x, dict) and x.get("receipt_id") == a["receipt"]), None)
|
||
if r is None:
|
||
if on("receipt-given"):
|
||
raise Refuse("receipt_missing", at)
|
||
continue
|
||
if on("receipt-verifies") and OER.verify_receipt(r, keys):
|
||
raise Refuse("receipt_invalid", at)
|
||
status = 1 if a["outcome"] == "served" else -1
|
||
work = r.get("work") if isinstance(r.get("work"), dict) else {}
|
||
got = work.get("status")
|
||
status_ok = isinstance(got, int) and not isinstance(got, bool) and got == status
|
||
if on("receipt-matches") and (r.get("kind") != "job" or r.get("capability") != "oer.job." + rec["operation"] or not status_ok):
|
||
raise Refuse("receipt_mismatch", at)
|
||
# Version 2 (§6): the served hop's receipt names the outputs the attempt states.
|
||
if on("receipt-output") and a["outcome"] == "served" and "outputs" in a:
|
||
subject = r.get("subject") if isinstance(r.get("subject"), dict) else {}
|
||
if subject.get("output") != commitment(a["outputs"]):
|
||
raise Refuse("receipt_output_mismatch", at)
|
||
# Version 3 (§6): it names the root the attempt states.
|
||
if on("receipt-root") and a["outcome"] == "served" and "delivered" in a:
|
||
subject = r.get("subject") if isinstance(r.get("subject"), dict) else {}
|
||
if subject.get("output") != a["delivered"]["root"]:
|
||
raise Refuse("receipt_output_mismatch", at)
|
||
if OER.is_unmetered(r):
|
||
unmetered.append(a["hop"])
|
||
return unmetered
|
||
|
||
|
||
# ---------------------------------------------------------------- §6.2 third-party hop records
|
||
|
||
THIRD_DOMAIN = b"wai:route-third-party-hop\x01"
|
||
THIRD = ["kind", "version", "job", "hop", "endpoint", "provider", "receipt", "origin", "account", "provider_job", "outcome",
|
||
"error", "exchanges", "outputs", "usage", "charge", "start", "end", "signer", "sig"]
|
||
EXCHANGE = ["method", "url", "start", "end", "sent", "status", "received", "json"]
|
||
ORIGIN = re.compile(r"\Ahttps://[a-z0-9.-]{1,253}(?::[0-9]{1,5})?\Z")
|
||
METHOD = re.compile(r"\A[A-Z]{1,16}\Z")
|
||
EMPTY_SHA256 = hashlib.sha256(b"").hexdigest()
|
||
|
||
|
||
def is_text(v, top):
|
||
return isinstance(v, str) and 1 <= len(v.encode("utf-8")) <= top
|
||
|
||
|
||
def url_ok(v):
|
||
if not isinstance(v, str) or "?" in v or "#" in v:
|
||
return False
|
||
for scheme in ("https://", "http://"):
|
||
if v.startswith(scheme):
|
||
return 1 <= len(v[len(scheme):].encode("utf-8")) <= 2040
|
||
return False
|
||
|
||
|
||
def is_link(s):
|
||
return s.startswith("https://") or s.startswith("http://")
|
||
|
||
|
||
def keeps_a_query(v):
|
||
"""A string, a member's name among them, that begins as a URL and holds a `?` or a `#`."""
|
||
if isinstance(v, str):
|
||
return is_link(v) and ("?" in v or "#" in v)
|
||
if isinstance(v, list):
|
||
return any(keeps_a_query(x) for x in v)
|
||
if isinstance(v, dict):
|
||
return any(keeps_a_query(k) or keeps_a_query(x) for k, x in v.items())
|
||
return False
|
||
|
||
|
||
def body_ok(v):
|
||
return exactly(v, ["sha256", "bytes"]) and is_hex(v["sha256"], 64) and is_int(v["bytes"])
|
||
|
||
|
||
def exchange_ok(x, previous, end):
|
||
if not exactly(x, EXCHANGE):
|
||
return False
|
||
if on("tp-method") and not (isinstance(x["method"], str) and METHOD.match(x["method"])):
|
||
return False
|
||
if on("tp-url") and not url_ok(x["url"]):
|
||
return False
|
||
if not (is_int(x["start"]) and is_int(x["end"])):
|
||
return False
|
||
if on("tp-exchange-times") and x["start"] > x["end"]:
|
||
return False
|
||
if on("tp-past-end") and x["end"] > end:
|
||
return False
|
||
if on("tp-exchange-order") and x["start"] < previous:
|
||
return False
|
||
if on("tp-body") and not (body_ok(x["sent"]) and body_ok(x["received"])):
|
||
return False
|
||
if on("tp-status") and not (is_int(x["status"]) and (x["status"] == 0 or 100 <= x["status"] <= 599)):
|
||
return False
|
||
if on("tp-no-response") and x["status"] == 0 and (x["received"] != {"sha256": EMPTY_SHA256, "bytes": 0} or x["json"] is not None):
|
||
return False
|
||
if on("tp-kept-query") and keeps_a_query(x["json"]):
|
||
return False
|
||
return True
|
||
|
||
|
||
def usage_ok(u):
|
||
if not exactly(u, ["unit", "quantity", "source"]):
|
||
return False
|
||
return (not on("tp-usage-unit") or is_unit(u["unit"])) and (not on("tp-usage-quantity") or is_int(u["quantity"])) \
|
||
and (not on("tp-usage-source") or is_text(u["source"], 512))
|
||
|
||
|
||
def charge_ok(c):
|
||
if not exactly(c, ["amount", "unit", "source"]):
|
||
return False
|
||
return (not on("tp-charge-amount") or is_text(c["amount"], 64)) and (not on("tp-charge-unit") or is_text(c["unit"], 64)) \
|
||
and (not on("tp-charge-source") or is_text(c["source"], 512))
|
||
|
||
|
||
def third_outputs_ok(v):
|
||
if not isinstance(v, list):
|
||
return False
|
||
names, hashes = set(), set()
|
||
for e in v:
|
||
if not (isinstance(e, list) and len(e) == 2 and isinstance(e[0], str) and isinstance(e[1], str)):
|
||
return False
|
||
if on("tp-name-form") and not is_text(e[0], 255):
|
||
return False
|
||
if on("tp-name-slash") and "/" in e[0]:
|
||
return False
|
||
if on("tp-hash-form") and not is_hex(e[1], 64):
|
||
return False
|
||
if on("tp-name-twice") and e[0] in names:
|
||
return False
|
||
if on("tp-object-twice") and e[1] in hashes:
|
||
return False
|
||
names.add(e[0])
|
||
hashes.add(e[1])
|
||
return True
|
||
|
||
|
||
def third_party_form(r):
|
||
if on("tp-members") and not exactly(r, THIRD):
|
||
return False
|
||
if not exactly(r, THIRD):
|
||
return True
|
||
if on("tp-version-form") and not (is_int(r["version"]) and r["version"] == 1):
|
||
return False
|
||
if not (is_int(r["start"]) and is_int(r["end"]) and isinstance(r["exchanges"], list)):
|
||
return False
|
||
if on("tp-exchanges-number") and not r["exchanges"]:
|
||
return False
|
||
if on("tp-most-exchanges") and len(r["exchanges"]) > 1024:
|
||
return False
|
||
previous = r["start"]
|
||
for x in r["exchanges"]:
|
||
if not exchange_ok(x, previous, r["end"]):
|
||
return False
|
||
if isinstance(x, dict) and is_int(x.get("start")):
|
||
previous = x["start"]
|
||
if on("tp-outputs-form") and not third_outputs_ok(r["outputs"]):
|
||
return False
|
||
if on("tp-outcome-form"):
|
||
if r["outcome"] == "served":
|
||
if not (r["error"] is None and isinstance(r["outputs"], list) and len(r["outputs"]) >= 1 and usage_ok(r["usage"])):
|
||
return False
|
||
if on("tp-most-outputs") and len(r["outputs"]) > 1024:
|
||
return False
|
||
elif r["outcome"] == "failed":
|
||
err = r["error"]
|
||
if not (exactly(err, ["class", "code"]) and r["outputs"] == [] and (r["usage"] is None or usage_ok(r["usage"]))):
|
||
return False
|
||
if on("tp-error-class") and err["class"] not in ERRORS:
|
||
return False
|
||
if on("tp-error-code") and not is_text(err["code"], 256):
|
||
return False
|
||
else:
|
||
return False
|
||
if on("tp-hop-form") and not (is_int(r["hop"]) and r["hop"] >= 1):
|
||
return False
|
||
if on("tp-origin") and not (isinstance(r["origin"], str) and ORIGIN.match(r["origin"])):
|
||
return False
|
||
if on("tp-account") and r["account"] not in ("operator", "customer"):
|
||
return False
|
||
if on("tp-charge-form") and not (r["charge"] is None or charge_ok(r["charge"])):
|
||
return False
|
||
if on("tp-signer-form") and not (is_hex(r["signer"], 64) and is_hex(r["sig"], 128)):
|
||
return False
|
||
if on("tp-job-form") and not is_token(r["job"]):
|
||
return False
|
||
if on("tp-endpoint-form") and not is_token(r["endpoint"]):
|
||
return False
|
||
if on("tp-provider-form") and not is_token(r["provider"]):
|
||
return False
|
||
if on("tp-receipt-form") and not (isinstance(r["receipt"], str) and RECEIPT_ID.match(r["receipt"])):
|
||
return False
|
||
if on("tp-provider-job") and not (r["provider_job"] is None or is_text(r["provider_job"], 256)):
|
||
return False
|
||
return True
|
||
|
||
|
||
def third_party_records(rec, texts, receipt_texts):
|
||
"""§7.2 step 9: each record read, its signature checked, and held to its hop."""
|
||
receipts = [OER.load(io.StringIO(t)) for t in receipt_texts]
|
||
seen = set()
|
||
out = []
|
||
for i, t in enumerate(texts):
|
||
place = ["third_party", i]
|
||
r = read_document(t)
|
||
if not isinstance(r, dict) or r.get("kind") != "wai-route-third-party-hop":
|
||
raise Refuse("malformed", place)
|
||
if on("tp-version") and is_int(r.get("version")) and r["version"] != 1:
|
||
raise Refuse("unknown_version", place + ["version"])
|
||
if on("tp-form") and not third_party_form(r):
|
||
raise Refuse("third_party_invalid", place)
|
||
if on("tp-signature"):
|
||
body = {k: v for k, v in r.items() if k != "sig"}
|
||
bad = ed_verify(bytes.fromhex(r["signer"]), THIRD_DOMAIN + canonical(body).encode("utf-8"), bytes.fromhex(r["sig"]))
|
||
if bad:
|
||
raise Refuse(bad, place + ["sig"])
|
||
mismatch = lambda m: Refuse("third_party_mismatch", place + [m])
|
||
if on("tp-signer") and r["signer"] != rec["signer"]:
|
||
raise mismatch("signer")
|
||
if on("tp-job") and r["job"] != rec["job"]:
|
||
raise mismatch("job")
|
||
atts = rec["attempts"]
|
||
if r["hop"] > len(atts):
|
||
if on("tp-hop"):
|
||
raise mismatch("hop")
|
||
continue
|
||
if on("tp-one-a-hop") and r["hop"] in seen:
|
||
raise mismatch("hop")
|
||
seen.add(r["hop"])
|
||
a = atts[r["hop"] - 1]
|
||
for m in ("endpoint", "receipt", "outcome"):
|
||
if on("tp-" + m) and r[m] != a[m]:
|
||
raise mismatch(m)
|
||
if on("tp-error") and r["outcome"] == "failed" and r["error"]["class"] != a["error"]:
|
||
raise mismatch("error")
|
||
c = next((x for x in rec["candidates"] if x["endpoint"] == r["endpoint"]), None)
|
||
if on("tp-provider") and (c is None or r["provider"] != c["provider"]):
|
||
raise mismatch("provider")
|
||
if on("tp-unit") and r["usage"] is not None and (c is None or r["usage"]["unit"] != c["unit"]):
|
||
raise mismatch("usage")
|
||
if on("tp-start") and r["start"] < a["start"]:
|
||
raise mismatch("start")
|
||
if on("tp-end") and r["end"] > a["end"]:
|
||
raise mismatch("end")
|
||
if r["outcome"] == "served" and a["outcome"] == "served":
|
||
root = commitment([e[1] for e in r["outputs"]])
|
||
receipt = next(x for x in receipts if isinstance(x, dict) and x.get("receipt_id") == a["receipt"])
|
||
subject = receipt.get("subject") if isinstance(receipt.get("subject"), dict) else {}
|
||
if on("tp-root") and subject.get("output") != root:
|
||
raise Refuse("third_party_output_mismatch", place + ["outputs"])
|
||
if on("tp-count") and "delivered" in a and a["delivered"]["count"] != len(r["outputs"]):
|
||
raise Refuse("third_party_output_mismatch", place + ["outputs"])
|
||
out.append({"hop": r["hop"], "digest": digest(r), "account": r["account"], "usage": r["usage"], "charge": r["charge"]})
|
||
out.sort(key=lambda x: x["hop"])
|
||
return out
|
||
|
||
|
||
# ---------------------------------------------------------------- §6.3 a delivery's decision
|
||
|
||
DELIVERY_DOMAIN = b"wai:route-delivery-decision\x01"
|
||
DELIVERY = ["kind", "version", "job", "parent", "seq", "prev", "objects", "delivery", "operation", "capability", "fallback",
|
||
"quantities", "request", "policy", "pricebook", "candidates", "order", "decision", "route", "passed", "decided",
|
||
"signer", "sig"]
|
||
PASSED_KEYS = ["targets", "price", "ttfb", "energy", "quality", "endpoint"] # §4.4
|
||
OFFER = ["entry", "unit", "quantity", "cost", "energy", "energy_class", "energy_ppm", "grade"] # §6.4
|
||
GRADES = ["projected", "simulated", "derived", "measured"] # oer-receipts §2, weakest first
|
||
GRADE_OF = {"CalibratedInstrument": "measured", "OnChipCounter": "measured", "ModelBased": "derived", "Estimator": "projected"}
|
||
PRICE_EXCEEDED = "price_exceeded" # §6.4
|
||
|
||
|
||
def offer_form(o):
|
||
"""§6.4: an offer's members, each of its own form."""
|
||
return (exactly(o, OFFER) and is_int(o["entry"]) and is_unit(o["unit"]) and is_int(o["quantity"], 1)
|
||
and null_or(o["cost"], is_int) and null_or(o["energy"], is_int)
|
||
and null_or(o["energy_class"], lambda c: c in CLASSES) and ppm_ok(o["energy_ppm"])
|
||
and null_or(o["grade"], lambda g: g in GRADES))
|
||
|
||
|
||
def book_of_decision(version):
|
||
"""§3.4: the price book version a decision of `version` rests on."""
|
||
return 3 if version == 2 else 2
|
||
|
||
|
||
def delivery_form(d):
|
||
"""§6.3: the members, each of its own form. Relations between them are step 10's."""
|
||
version = d.get("version") if isinstance(d, dict) else None
|
||
known = is_int(version) and version in (1, 2)
|
||
if not exactly(d, DELIVERY + (["offer"] if known and version == 2 else [])):
|
||
return False
|
||
o = d["objects"]
|
||
return (known and (version == 1 or null_or(d["offer"], offer_form))
|
||
and is_token(d["job"]) and is_hex(d["parent"], 64) and is_int(d["seq"], 1)
|
||
and null_or(d["prev"], lambda x: is_hex(x, 64))
|
||
and exactly(o, ["count", "root"]) and is_int(o["count"], 1) and is_hex(o["root"], 64)
|
||
and is_token(d["delivery"]) and isinstance(d["operation"], str) and OPERATION.match(d["operation"]) is not None
|
||
and is_cap(d["capability"]) and null_or(d["fallback"], is_cap) and quantities_ok(d["quantities"])
|
||
and is_hex(d["request"], 64)
|
||
and exactly(d["policy"], ["name", "version", "digest"]) and is_token(d["policy"]["name"])
|
||
and is_int(d["policy"]["version"], 1) and is_hex(d["policy"]["digest"], 64)
|
||
and exactly(d["pricebook"], ["digest", "currency", "issued"]) and is_hex(d["pricebook"]["digest"], 64)
|
||
and isinstance(d["pricebook"]["currency"], str) and CURRENCY.match(d["pricebook"]["currency"]) is not None
|
||
and is_int(d["pricebook"]["issued"])
|
||
and isinstance(d["candidates"], list) and len(d["candidates"]) <= 1024 and all(candidate_form(c, book_of_decision(version)) for c in d["candidates"])
|
||
and isinstance(d["order"], list) and len(d["order"]) <= 1024 and all(is_token(x) for x in d["order"])
|
||
and exactly(d["decision"], ["decided_by", "runner_up"]) and d["decision"]["decided_by"] in DECIDED_BY
|
||
and null_or(d["decision"]["runner_up"], is_token)
|
||
and null_or(d["route"], is_token)
|
||
and isinstance(d["passed"], list) and len(d["passed"]) <= 1023
|
||
and all(exactly(x, ["endpoint", "key"]) and is_token(x["endpoint"]) and x["key"] in PASSED_KEYS for x in d["passed"])
|
||
and is_int(d["decided"]) and is_hex(d["signer"], 64) and is_hex(d["sig"], 128))
|
||
|
||
|
||
def passed_over(req, pol, cands):
|
||
"""§6.3 `passed`: each survivor after the first, in the order, with the first key of the key
|
||
list on which it and the first differ."""
|
||
survivors = sorted((c for c in cands if c["rank"] is not None), key=lambda c: c["rank"])
|
||
if not survivors:
|
||
return []
|
||
out = []
|
||
for c in survivors[1:]:
|
||
key = next(k for k in key_list(req, pol) if compare(req, k, survivors[0], c))
|
||
out.append({"endpoint": c["endpoint"], "key": key})
|
||
return out
|
||
|
||
|
||
def delivered_by(rec):
|
||
"""What the record's served attempt delivered: (count, root, end), or None for a record of
|
||
version 1 and one no hop served."""
|
||
atts = rec["attempts"]
|
||
if not atts or atts[-1]["outcome"] != "served":
|
||
return None
|
||
a = atts[-1]
|
||
if "delivered" in a:
|
||
return a["delivered"]["count"], a["delivered"]["root"], a["end"]
|
||
if "outputs" in a:
|
||
return len(a["outputs"]), commitment(a["outputs"]), a["end"]
|
||
return None
|
||
|
||
|
||
def offer_of(req, book, cands, order):
|
||
"""§6.4: what the first of the order was taken at, or None when no route survives."""
|
||
if not order:
|
||
return None
|
||
c = next(x for x in cands if x["endpoint"] == order[0])
|
||
place = next(i for i, e in enumerate(book["entries"]) if e["endpoint"] == c["endpoint"] and e["operation"] == c["operation"])
|
||
return {"entry": place, "unit": c["unit"], "quantity": quantity(req, c["unit"]), "cost": c["cost"], "energy": c["energy"],
|
||
"energy_class": c["energy_class"], "energy_ppm": c["energy_ppm"], "grade": GRADE_OF.get(c["energy_class"])}
|
||
|
||
|
||
def micro_joules(v):
|
||
"""A receipt's figure in whole microjoules, rounded to the nearest and a half to the even one
|
||
(round() on a Fraction does exactly that); None when it is negative or past what a u64 holds."""
|
||
n = round(Fraction(str(v)) * 10 ** 6)
|
||
return n if 0 <= n < 2 ** 64 and Fraction(str(v)) >= 0 else None
|
||
|
||
|
||
def held_to_offer(text, d, decision_digest, keys, here):
|
||
"""§6.4: a delivery's receipt held to its decision and its offer. What it gave (§7.3)."""
|
||
try:
|
||
r = OER.load(io.StringIO(text))
|
||
except ValueError:
|
||
raise Refuse("receipt_invalid", here)
|
||
if on("dr-verifies") and OER.verify_receipt(r, keys):
|
||
raise Refuse("receipt_invalid", here)
|
||
subject = r.get("subject") if isinstance(r.get("subject"), dict) else {}
|
||
work = r.get("work") if isinstance(r.get("work"), dict) else {}
|
||
chain = r.get("chain") if isinstance(r.get("chain"), dict) else {}
|
||
status = work.get("status")
|
||
inputs = subject.get("inputs") if isinstance(subject.get("inputs"), list) else []
|
||
for check_, holds in (("dr-kind", r.get("kind") == "job"),
|
||
("dr-operation", r.get("capability") == "oer.job." + d["operation"]),
|
||
("dr-status", isinstance(status, int) and not isinstance(status, bool) and status == 1),
|
||
("dr-route", chain.get("signer") == d["route"]),
|
||
("dr-decision", "sha256:" + decision_digest in inputs)):
|
||
if on(check_) and not holds:
|
||
raise Refuse("receipt_mismatch", here)
|
||
if on("dr-output") and subject.get("output") != d["objects"]["root"]:
|
||
raise Refuse("receipt_output_mismatch", here)
|
||
offer = d["offer"]
|
||
how = r["provenance"]["how"]
|
||
if on("dr-grade") and offer["grade"] is not None and GRADES.index(how) > GRADES.index(offer["grade"]):
|
||
raise Refuse("delivery_grade_exceeded", here)
|
||
figure = None if OER.is_unmetered(r) else micro_joules(r["energy"]["joules_total"])
|
||
if on("dr-figure") and figure is None and not OER.is_unmetered(r):
|
||
raise Refuse("receipt_mismatch", here)
|
||
ppm = (offer["energy_ppm"] or 0) if on("dr-uncertainty") else 0
|
||
allowed = None if offer["energy"] is None else offer["energy"] * (10 ** 6 + ppm)
|
||
above = figure is not None and allowed is not None and figure * 10 ** 6 > allowed
|
||
flagged = work.get("outcome") == PRICE_EXCEEDED
|
||
if on("dr-unflagged") and above and not flagged:
|
||
raise Refuse("delivery_price_unflagged", here)
|
||
if on("dr-misflagged") and flagged and not above:
|
||
raise Refuse("delivery_price_misflagged", here)
|
||
verdict = "no_figure" if allowed is None else "unmetered" if figure is None else "exceeded" if above else "within"
|
||
return {"verdict": verdict, "receipt": r["receipt_id"], "energy": figure, "grade": how}
|
||
|
||
|
||
def delivery_decisions(rec, entries, keys):
|
||
"""§7.2 step 10: a job's decisions, from the first and in order."""
|
||
parent = delivered_by(rec)
|
||
out, before, seen = [], None, set()
|
||
for i, t in enumerate(entries):
|
||
place = ["deliveries", i]
|
||
req = read(t["request"], request_ok, "request_invalid", place + ["request"])
|
||
book = read(t["pricebook"], pricebook_ok, "pricebook_invalid", place + ["pricebook"])
|
||
pol = read(t["policy"], policy_ok, "policy_invalid", place + ["policy"])
|
||
d = read_document(t["decision"])
|
||
at = lambda m: place + ["decision", m] # noqa: E731
|
||
if not isinstance(d, dict) or d.get("kind") != "wai-route-delivery-decision":
|
||
raise Refuse("malformed", place + ["decision"])
|
||
if on("dd-version") and is_int(d.get("version")) and d["version"] not in (1, 2):
|
||
raise Refuse("unknown_version", at("version"))
|
||
if on("dd-form") and not delivery_form(d):
|
||
raise Refuse("delivery_invalid", place + ["decision"])
|
||
if on("dd-signature"):
|
||
body = {k: v for k, v in d.items() if k != "sig"}
|
||
bad = ed_verify(bytes.fromhex(d["signer"]), DELIVERY_DOMAIN + canonical(body).encode("utf-8"), bytes.fromhex(d["sig"]))
|
||
if bad:
|
||
raise Refuse(bad, at("sig"))
|
||
# Its documents.
|
||
for m, want in (("delivery", req["job"]), ("operation", req["operation"]), ("capability", req["capability"]),
|
||
("fallback", req["fallback"]), ("quantities", req["quantities"]), ("request", digest(req))):
|
||
if on("dd-request-" + m) and not same(d[m], want):
|
||
raise Refuse("request_mismatch", at(m))
|
||
for m, want in (("name", pol["name"]), ("version", pol["version"]), ("digest", digest(pol))):
|
||
if on("dd-policy-" + m) and not same(d["policy"][m], want):
|
||
raise Refuse("policy_mismatch", at("policy"))
|
||
if on("dd-pricebook-version") and book_version(book) != book_of_decision(d["version"]):
|
||
raise Refuse("pricebook_mismatch", at("pricebook"))
|
||
for m, want in (("digest", digest(book)), ("currency", book["currency"]), ("issued", book["issued"])):
|
||
if on("dd-pricebook-" + m) and not same(d["pricebook"][m], want):
|
||
raise Refuse("pricebook_mismatch", at("pricebook"))
|
||
# §4 over them.
|
||
cands, order, decision, _ = route(req, book, pol)
|
||
stated = d["candidates"]
|
||
for j in range(max(len(stated), len(cands))):
|
||
here = place + ["decision", "candidates", j]
|
||
if j >= len(stated) or j >= len(cands):
|
||
raise Refuse("candidate_mismatch", here)
|
||
s_, c = stated[j], cands[j]
|
||
if on("dd-candidates") and not same({k: v for k, v in s_.items() if k not in ("removed", "rank")},
|
||
{k: v for k, v in c.items() if k not in ("removed", "rank")}):
|
||
raise Refuse("candidate_mismatch", here)
|
||
if on("dd-removals") and not same(s_["removed"], c["removed"]):
|
||
raise Refuse("removal_mismatch", here)
|
||
if on("dd-ranks") and not same(s_["rank"], c["rank"]):
|
||
raise Refuse("rank_mismatch", here)
|
||
if on("dd-order") and not same(d["order"], order):
|
||
raise Refuse("rank_mismatch", at("order"))
|
||
if on("dd-decision") and not same(d["decision"], decision):
|
||
raise Refuse("decision_mismatch", at("decision"))
|
||
# Its route.
|
||
route_ = order[0] if order else None
|
||
if on("dd-route") and not same(d["route"], route_):
|
||
raise Refuse("delivery_route_mismatch", at("route"))
|
||
passed = passed_over(req, pol, cands)
|
||
if on("dd-passed") and not same(d["passed"], passed):
|
||
raise Refuse("delivery_route_mismatch", at("passed"))
|
||
# Its offer.
|
||
if d["version"] == 2:
|
||
offer = offer_of(req, book, cands, order)
|
||
if on("dd-offer") and (offer is None) != (d["offer"] is None):
|
||
raise Refuse("delivery_offer_mismatch", at("offer"))
|
||
for m in OFFER if offer is not None and d["offer"] is not None else ():
|
||
if on("dd-offer-" + m) and not same(d["offer"][m], offer[m]):
|
||
raise Refuse("delivery_offer_mismatch", at("offer"))
|
||
# Its parent.
|
||
for m in ("signer", "job"):
|
||
if on("dd-parent-" + m) and d[m] != rec[m]:
|
||
raise Refuse("delivery_parent_mismatch", at(m))
|
||
if on("dd-parent") and d["parent"] != digest(rec):
|
||
raise Refuse("delivery_parent_mismatch", at("parent"))
|
||
if parent is None:
|
||
raise Refuse("delivery_parent_mismatch", at("objects"))
|
||
count, root, served_end = parent
|
||
if on("dd-objects") and not same(d["objects"], {"count": count, "root": root}):
|
||
raise Refuse("delivery_parent_mismatch", at("objects"))
|
||
# Its chain.
|
||
if on("dd-seq") and d["seq"] != i + 1:
|
||
raise Refuse("delivery_chain_broken", at("seq"))
|
||
if on("dd-prev") and d["prev"] != (before[0] if before else None):
|
||
raise Refuse("delivery_chain_broken", at("prev"))
|
||
if on("dd-decided") and (d["decided"] < served_end or (before is not None and d["decided"] < before[1])):
|
||
raise Refuse("delivery_chain_broken", at("decided"))
|
||
if on("dd-once") and req["job"] in seen:
|
||
raise Refuse("delivery_chain_broken", at("delivery"))
|
||
seen.add(req["job"])
|
||
# Its receipt.
|
||
conformance = {"verdict": "not_checked", "receipt": None, "energy": None, "grade": None}
|
||
if "receipt" in t:
|
||
if on("dr-version") and d["version"] != 2:
|
||
raise Refuse("delivery_offer_mismatch", at("version"))
|
||
if on("dr-a-route") and route_ is None:
|
||
raise Refuse("delivery_offer_mismatch", place + ["receipt"])
|
||
conformance = held_to_offer(t["receipt"], d, digest(d), keys, place + ["receipt"])
|
||
before = (digest(d), d["decided"])
|
||
read_ = {"seq": i + 1, "delivery": req["job"], "digest": digest(d), "route": route_, "decision": decision, "passed": passed}
|
||
if d["version"] == 2:
|
||
read_.update(offer=d["offer"], conformance=conformance)
|
||
out.append(read_)
|
||
return out
|
||
|
||
|
||
# ---------------------------------------------------------------- the corpus
|
||
|
||
def check(doc, quiet=False):
|
||
fails = []
|
||
for v in doc.get("vectors", []):
|
||
try:
|
||
got = verify(v["document"])
|
||
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"))
|
||
if quiet: # a mutated copy or a weakened verifier only asks whether anything fails
|
||
break
|
||
return fails
|
||
|
||
|
||
def reasons_listed():
|
||
"""§8's table, in order, and the codes its paragraph names as reused."""
|
||
text = open(EXTENSION, encoding="utf-8").read()
|
||
start = text.index("\n## 8. Reasons")
|
||
section = text[start:text.find("\n## ", start + 1)]
|
||
added = [m.group(1) for m in re.finditer(r"^\| `([a-z_]+)` \|", section, re.M)]
|
||
reused = [r for r in re.findall(r"`([a-z_]+)`", section.split("also raises", 1)[1]) if r not in added]
|
||
return added, reused
|
||
|
||
|
||
def flip(text, at):
|
||
b = bytearray(text.encode("utf-8"))
|
||
i = at % len(b)
|
||
b[i] = ord("1") if b[i] != ord("1") else ord("2")
|
||
return b.decode("utf-8", "replace")
|
||
|
||
|
||
def mutations(doc):
|
||
"""Copies of the corpus, one defect each, that a verifier must reject."""
|
||
out = []
|
||
|
||
def copy_with(name, k, edit):
|
||
m = copy.deepcopy(doc)
|
||
edit(m["vectors"][k])
|
||
out.append(("%s: %s" % (doc["vectors"][k]["name"], name), m))
|
||
|
||
for k, v in enumerate(doc["vectors"]):
|
||
d, o = v["document"], v["outcome"]
|
||
if o["outcome"] != "accept":
|
||
r = o["reason"]
|
||
copy_with("its stated reason changed", k, lambda x, r=r: x["outcome"].__setitem__("reason", "record_invalid" if r != "record_invalid" else "malformed"))
|
||
copy_with("its stated place changed", k, lambda x: x["outcome"].__setitem__("at", ["record", "elsewhere"]))
|
||
continue
|
||
for member in ("request", "pricebook", "policy", "record"):
|
||
digits = [i for i, c in enumerate(d[member]) if c in "23456789"]
|
||
if digits:
|
||
copy_with("a digit of its %s changed" % member, k, lambda x, m=member, i=digits[len(digits) // 2]:
|
||
x["document"].__setitem__(m, x["document"][m][:i] + str(int(x["document"][m][i]) - 1) + x["document"][m][i + 1:]))
|
||
at_sig = d["record"].index('"sig":"') + len('"sig":"')
|
||
copy_with("its record's signature flipped", k, lambda x, i=at_sig: x["document"].__setitem__(
|
||
"record", d["record"][:i] + ("1" if d["record"][i] == "0" else "0") + d["record"][i + 1:]))
|
||
if d["receipts"]:
|
||
copy_with("its receipts dropped", k, lambda x: x["document"].__setitem__("receipts", []))
|
||
copy_with("its first receipt's figure changed", k, lambda x: x["document"]["receipts"].__setitem__(
|
||
0, x["document"]["receipts"][0].replace('"latency_s":"1.0"', '"latency_s":"2.0"', 1)))
|
||
copy_with("its keys swapped for another", k, lambda x: x["document"].__setitem__(
|
||
"keys", {n: "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" for n in x["document"]["keys"]}))
|
||
copy_with("its stated served hop changed", k, lambda x: x["outcome"].__setitem__("served", None if o["served"] else {"endpoint": "im.alpha", "hop": 1}))
|
||
copy_with("its stated billed hop changed", k, lambda x: x["outcome"].__setitem__("billed", None if o["billed"] else 1))
|
||
if o["served"] and "outputs" in o["served"]:
|
||
copy_with("its stated outputs reversed and one more", k, lambda x: x["outcome"]["served"].__setitem__(
|
||
"outputs", list(reversed(o["served"]["outputs"])) + ["00" * 32]))
|
||
copy_with("its served hop's outputs dropped from the outcome", k, lambda x: x["outcome"]["served"].pop("outputs"))
|
||
if o["served"] and "delivered" in o["served"]:
|
||
copy_with("its stated count one more", k, lambda x: x["outcome"]["served"]["delivered"].__setitem__("count", o["served"]["delivered"]["count"] + 1))
|
||
copy_with("its stated root another", k, lambda x: x["outcome"]["served"]["delivered"].__setitem__("root", "00" * 32))
|
||
copy_with("its served hop's delivered dropped from the outcome", k, lambda x: x["outcome"]["served"].pop("delivered"))
|
||
if "third_party" in o and o["third_party"]:
|
||
if o["third_party"][-1]["usage"] is not None:
|
||
copy_with("its stated third-party usage dropped", k, lambda x: x["outcome"]["third_party"][-1].__setitem__("usage", None))
|
||
copy_with("its stated third-party charge changed", k, lambda x: x["outcome"]["third_party"][0].__setitem__("charge", {"amount": "1", "unit": "x", "source": "y"}))
|
||
copy_with("its stated third-party account changed", k, lambda x: x["outcome"]["third_party"][0].__setitem__("account", "someone"))
|
||
copy_with("its stated third-party digest changed", k, lambda x: x["outcome"]["third_party"][0].__setitem__("digest", "00" * 32))
|
||
copy_with("a record's body named by another digest", k, lambda x: x["document"]["third_party"].__setitem__(
|
||
0, x["document"]["third_party"][0].replace('"bytes":', '"bytes":1', 1)))
|
||
if "deliveries" in o and o["deliveries"]:
|
||
copy_with("its stated route changed", k, lambda x: x["outcome"]["deliveries"][0].__setitem__("route", "relay.us" if o["deliveries"][0]["route"] != "relay.us" else None))
|
||
copy_with("its stated passed routes dropped", k, lambda x: x["outcome"]["deliveries"][0].__setitem__("passed", [] if o["deliveries"][0]["passed"] else [{"endpoint": "x", "key": "price"}]))
|
||
copy_with("its stated decision's digest changed", k, lambda x: x["outcome"]["deliveries"][0].__setitem__("digest", "00" * 32))
|
||
copy_with("its stated delivery's seq changed", k, lambda x: x["outcome"]["deliveries"][-1].__setitem__("seq", 7))
|
||
last = o["deliveries"][-1]
|
||
if "offer" in last:
|
||
copy_with("its stated offer dropped", k, lambda x: x["outcome"]["deliveries"][-1].pop("offer"))
|
||
copy_with("its stated conformance dropped", k, lambda x: x["outcome"]["deliveries"][-1].pop("conformance"))
|
||
other = "exceeded" if last["conformance"]["verdict"] != "exceeded" else "within"
|
||
copy_with("its stated verdict changed", k, lambda x, c=other: x["outcome"]["deliveries"][-1]["conformance"].__setitem__("verdict", c))
|
||
copy_with("its stated figure changed by one", k, lambda x, e=last["conformance"]["energy"]:
|
||
x["outcome"]["deliveries"][-1]["conformance"].__setitem__("energy", 1 if e is None else e + 1))
|
||
copy_with("its stated grade changed", k, lambda x, g=last["conformance"]["grade"]:
|
||
x["outcome"]["deliveries"][-1]["conformance"].__setitem__("grade", "simulated" if g != "simulated" else "derived"))
|
||
given = d["deliveries"][-1] if d["deliveries"] else {}
|
||
if "receipt" in given:
|
||
copy_with("its receipt dropped", k, lambda x: x["document"]["deliveries"][-1].pop("receipt"))
|
||
at_sig = given["receipt"].index('"signature":"') + len('"signature":"')
|
||
copy_with("its receipt's signature flipped", k, lambda x, i=at_sig, t=given["receipt"]: x["document"]["deliveries"][-1].__setitem__(
|
||
"receipt", t[:i] + ("1" if t[i] == "0" else "0") + t[i + 1:]))
|
||
copy_with("its last decision dropped", k, lambda x: x["document"]["deliveries"].pop())
|
||
copy_with("a decision's text another", k, lambda x: x["document"]["deliveries"][0].__setitem__(
|
||
"decision", x["document"]["deliveries"][0]["decision"].replace('"decided":', '"decided":1', 1)))
|
||
if "deliveries" in o and not o["deliveries"]:
|
||
copy_with("its stated deliveries dropped", k, lambda x: x["outcome"].pop("deliveries"))
|
||
copy_with("its stated deadline changed", k, lambda x: x["outcome"].__setitem__("deadline", "met" if o["deadline"] != "met" else "missed"))
|
||
copy_with("its stated order reversed", k, lambda x: x["outcome"].__setitem__("order", list(reversed(o["order"])) + ["x"]))
|
||
copy_with("its stated decision changed", k, lambda x: x["outcome"]["decision"].__setitem__("decided_by", "price" if o["decision"]["decided_by"] != "price" else "ttfb"))
|
||
copy_with("its stated removals dropped", k, lambda x: x["outcome"].__setitem__("removed", o["removed"][1:] if o["removed"] else [{"endpoint": "x", "limits": ["ga"]}]))
|
||
changed = [] if o["unmetered"] is None else (None if o["unmetered"] == [] else [])
|
||
copy_with("its stated unmetered hops changed", k, lambda x, c=changed: x["outcome"].__setitem__("unmetered", c))
|
||
return out
|
||
|
||
|
||
CHECKS = ["request_invalid", "pricebook_invalid", "policy_invalid", "version", "record-form", "signature",
|
||
"binding-request", "binding-policy", "binding-pricebook", "candidates", "removals", "ranks", "order", "decision",
|
||
"allowed", "removed", "in-order", "served-last", "times", "billing", "complete",
|
||
"receipt-given", "receipt-verifies", "receipt-matches",
|
||
"pricebook-version", "failures-relations", "operation-candidates", "thin-basis", "unbounded", "expected-price",
|
||
"expected-ttfb", "expected-targets", "record-version", "outputs-form", "receipt-output", "delivered-form",
|
||
"receipt-root",
|
||
"tp-version", "tp-form", "tp-signature", "tp-members", "tp-version-form", "tp-exchanges-number", "tp-method", "tp-url",
|
||
"tp-exchange-times", "tp-past-end", "tp-exchange-order", "tp-body", "tp-status", "tp-no-response",
|
||
"tp-kept-query", "tp-outputs-form", "tp-name-twice", "tp-object-twice", "tp-outcome-form", "tp-hop-form", "tp-origin",
|
||
"tp-account", "tp-charge-form", "tp-signer-form", "tp-usage-unit", "tp-usage-quantity", "tp-usage-source", "tp-charge-amount",
|
||
"tp-charge-unit", "tp-charge-source", "tp-name-form", "tp-name-slash", "tp-hash-form", "tp-error-class", "tp-error-code",
|
||
"tp-job-form", "tp-endpoint-form", "tp-provider-form", "tp-receipt-form", "tp-provider-job", "tp-most-outputs",
|
||
"tp-most-exchanges", "tp-signer", "tp-job", "tp-hop", "tp-one-a-hop", "tp-endpoint",
|
||
"tp-receipt", "tp-outcome", "tp-error", "tp-provider", "tp-unit", "tp-start", "tp-end", "tp-root", "tp-count",
|
||
"limit-policy", "limit-unit", "limit-region", "limit-residency", "limit-training", "limit-retention", "limit-ga",
|
||
"limit-consent", "limit-unit-price", "limit-spend",
|
||
"dd-version", "dd-form", "dd-signature", "dd-request-delivery", "dd-request-operation", "dd-request-capability",
|
||
"dd-request-fallback", "dd-request-quantities", "dd-request-request", "dd-policy-name", "dd-policy-version",
|
||
"dd-policy-digest", "dd-pricebook-version", "dd-pricebook-digest", "dd-pricebook-currency", "dd-pricebook-issued",
|
||
"dd-candidates", "dd-removals",
|
||
"dd-ranks", "dd-order", "dd-decision", "dd-route", "dd-passed", "dd-parent-signer", "dd-parent-job", "dd-parent",
|
||
"dd-objects", "dd-seq", "dd-prev", "dd-decided", "dd-once",
|
||
"ppm-form", "ppm-class", "dd-offer", "dd-offer-entry", "dd-offer-unit", "dd-offer-quantity", "dd-offer-cost",
|
||
"dd-offer-energy", "dd-offer-energy_class", "dd-offer-energy_ppm", "dd-offer-grade",
|
||
"dr-version", "dr-a-route", "dr-verifies", "dr-kind", "dr-operation", "dr-status", "dr-route", "dr-decision", "dr-output",
|
||
"dr-grade", "dr-figure", "dr-uncertainty", "dr-unflagged", "dr-misflagged"]
|
||
|
||
|
||
def main(corpus_dir):
|
||
doc = json.load(open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8"))
|
||
fails = check(doc)
|
||
files = sorted(os.listdir(corpus_dir))
|
||
if files != ["README.md", "vectors.json"]:
|
||
fails.append("the corpus holds other files: %s" % files)
|
||
seen = {v["outcome"].get("reason") for v in doc["vectors"]}
|
||
added, reused = reasons_listed()
|
||
missing = [r for r in added + reused if r not in seen]
|
||
if missing:
|
||
fails.append("no vector is refused %s" % missing)
|
||
print("%s every reason of §8 and every code it reuses refuses a vector (%d added, %d reused)"
|
||
% ("PASS" if not missing else "FAIL", len(added), len(reused)))
|
||
unlisted = sorted(r for r in seen if r is not None and r not in added + reused)
|
||
if unlisted:
|
||
fails.append("§8 does not name %s, which the corpus raises" % unlisted)
|
||
print("%s §8 names every reason the corpus raises" % ("PASS" if not unlisted else "FAIL"))
|
||
copies = mutations(doc)
|
||
weak = [name for name, m in copies if not check(m, quiet=True)]
|
||
print("%s every mutated copy of the corpus fails (%d copies)" % ("PASS" if not weak else "FAIL", len(copies)))
|
||
fails += ["the mutated copy %r verifies" % w for w in weak]
|
||
undetected = []
|
||
for c in CHECKS:
|
||
SKIP.add(c)
|
||
try:
|
||
if not check(doc, quiet=True):
|
||
undetected.append(c)
|
||
finally:
|
||
SKIP.discard(c)
|
||
print("%s the corpus catches the verifier with each of its checks dropped (%d checks)" % ("PASS" if not undetected else "FAIL", len(CHECKS)))
|
||
fails += ["dropping the check %r goes unnoticed" % c for c in undetected]
|
||
for f in fails:
|
||
print("FAIL", f)
|
||
return 1 if fails else 0
|
||
|
||
|
||
def check_document(path):
|
||
"""One verification document: its outcome, printed. Status 0 only when it is accepted."""
|
||
got = verify(json.load(open(path, encoding="utf-8")))
|
||
print(json.dumps(got, indent=1))
|
||
return 0 if got["outcome"] == "accept" else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) == 3 and sys.argv[1] == "--document":
|
||
sys.exit(check_document(sys.argv[2]))
|
||
if len(sys.argv) != 2:
|
||
print("usage: route_record_verify.py <live-conformance/routes> | --document FILE", file=sys.stderr)
|
||
sys.exit(2)
|
||
sys.exit(main(sys.argv[1]))
|