open-standards/wai/oer-conformance/oer2_verify.py
Transaction Science 82473f6bcf oer: minor version 1, the unmetered marker and the acquisition class
A receipt whose oer is exactly "2.1" may state a null energy.joules_total, the
unmetered marker, reported as unmetered and never as a figure, and energy.class,
the figure's acquisition class with its evidence, held to the grade it supports.
Minor version 0 keeps its members, so no 2.0 receipt reads differently. Three
reasons (unmetered_invalid, class_invalid, class_grade_mismatch), the oer.job.*
namespace for job receipts, a third canonical fixture, job_vectors.json written by
hand-declared generate_job_vectors.py, and the differential fuzzer over both forms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:39:58 -04:00

597 lines
28 KiB
Python

#!/usr/bin/env python3
"""OER/2 reference verifier (draft 0.1 as profiled by extensions/oer-receipts.md).
Verifies receipts, chains, records, bundles and ledger identities against the
canonical-bytes, number, hashing and composition rules of the extension. Every failure is
an explicit reason; nothing is reinterpreted silently.
"""
import hashlib, io, json, math, re, sys
from decimal import (Context, Decimal, DivisionByZero, Inexact, InvalidOperation, MAX_EMAX,
MAX_PREC, MIN_EMIN, Overflow, localcontext)
FIELD_ORDER = {
"": ["oer", "receipt_id", "kind", "capability", "intent", "subject", "work", "energy", "provenance",
"settlement", "floor", "efficiency", "bundle", "chain", "hash_alg", "sig_alg"],
"subject": ["inputs", "output"],
"work": ["status", "outcome", "latency_s", "proficiency"],
"energy": ["joules_total", "boundary", "breakdown", "method", "temperature_k"],
"energy.breakdown": ["compute_j", "movement_j", "idle_alloc_j", "shared_alloc_j", "upstream_j"],
"provenance": ["how", "device", "weakest_input", "hardware_profile", "corpus"],
"provenance.weakest_input": ["how", "input"],
"settlement": ["question_key", "served_from_record", "record_id", "record_hash", "reuse_count", "derivation_j", "valid_until"],
"floor": ["bits_committed", "bits_delivered", "floor_j", "decades_above_floor"],
"efficiency": ["completed_units", "wasted_units", "joules_per_completed", "waste_fraction", "value_per_joule"],
"bundle": ["stages", "provenance_root"],
"bundle.stages[]": ["kind", "receipt_hash", "joules"],
"chain": ["prev_hash", "receipt_hash", "signer", "signature", "anchor"],
"chain.anchor": ["path", "delivered_at", "root"],
}
# Minor version 1 (§3.1): a receipt whose `oer` is exactly "2.1" may also carry the
# acquisition class of its figure, `energy.class`, and the unmetered marker, a `null`
# `energy.joules_total`. Every other receipt keeps the members above, so a 2.0 receipt
# reads exactly as it did.
MINOR_1 = "2.1"
FIELD_ORDER_MINOR_1 = dict(FIELD_ORDER)
FIELD_ORDER_MINOR_1["energy"] = FIELD_ORDER["energy"] + ["class"]
FIELD_ORDER_MINOR_1["energy.class"] = ["label", "relative_ppm", "window_us", "filtering", "calibration_ref"]
# The grade each class supports (§2), and the evidence each carries (§3.1).
CLASS_GRADE = {"CalibratedInstrument": "measured", "OnChipCounter": "measured", "ModelBased": "derived", "Estimator": "projected"}
CLASS_MEMBERS = {"CalibratedInstrument": {"label", "relative_ppm", "window_us", "calibration_ref"},
"OnChipCounter": {"label", "relative_ppm", "window_us", "filtering"},
"ModelBased": {"label"}, "Estimator": {"label"}}
FILTERING = {"Unknown", "Off", "On"}
KINDS = {"decision", "task", "reconstruction", "calibration", "job", "compile", "decode", "mitigate", "rearrange", "anchor", "bundle"}
GRADES = ["projected", "simulated", "derived", "measured"]
KBOLTZ, LN2 = Decimal("1.380649e-23"), Decimal("0.6931471805599453")
def _order(obj, path="", order=FIELD_ORDER):
if isinstance(obj, dict):
keys = order.get(path, sorted(obj.keys()))
out = {}
for k in keys:
if k in obj:
out[k] = _order(obj[k], (path + "." + k) if path else k, order)
for k in obj:
if k not in out:
raise ValueError("unknown field %s at %s" % (k, path or "root"))
return out
if isinstance(obj, list):
return [_order(x, path + "[]", order) for x in obj]
return obj
def field_order(receipt):
"""The members a receipt may carry: §3's, and for minor version 1 §3.1's too."""
return FIELD_ORDER_MINOR_1 if isinstance(receipt, dict) and receipt.get("oer") == MINOR_1 else FIELD_ORDER
def _dumps(v):
"""The one serialisation: compact separators, non-ASCII unescaped, UTF-8."""
return json.dumps(v, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def canonical_bytes(receipt):
body = {k: v for k, v in receipt.items() if k not in ("chain", "receipt_id")}
return _dumps(_order(body, "", field_order(receipt)))
def stage_list_bytes(stages):
"""The bytes `bundle.provenance_root` addresses: the stage list in member order,
serialised exactly as the body is."""
return _dumps([_order(s, "bundle.stages[]") for s in stages])
# The number profile (extension §4). A receipt body carries integers within I-JSON's
# +-(2**53 - 1) and decimal strings, nothing else: no fraction or exponent, no negative
# zero. Parsers disagree about the rest -- Python reads -0 and 2**64 as exact integers,
# serde_json reads both as floats -- so two verifiers would hash one receipt two ways.
MAX_SAFE = 2**53 - 1
def _int(s):
if s == "-0":
return -0.0 # a float, as serde_json reads it, so the number rule sees it
v = int(s)
float(v) # OverflowError past binary64, where serde_json refuses the document
return v
def _float(s):
v = float(s)
if not math.isfinite(v):
raise ValueError("number out of range: " + s)
return v
def _constant(s):
raise ValueError("not JSON: " + s)
MAX_DEPTH = 64
def _walk(v):
"""Every value in a document with its depth, iteratively: a document nested past
the interpreter's recursion limit is refused, not a crash."""
stack = [(v, 1)]
while stack:
x, d = stack.pop()
yield x, d
if isinstance(x, dict):
stack.extend((y, d + 1) for y in x.values())
stack.extend((k, d + 1) for k in x)
elif isinstance(x, list):
stack.extend((y, d + 1) for y in x)
def _depth(v):
return max((d for x, d in _walk(v) if isinstance(x, (dict, list))), default=0)
def _surrogate(v):
return any(isinstance(x, str) and any("\ud800" <= c <= "\udfff" for c in x) for x, _ in _walk(v))
def _no_duplicates(pairs):
seen = {}
for k, v in pairs:
if k in seen:
raise ValueError("duplicate member %r" % k)
seen[k] = v
return seen
def load(fp):
"""Read a vector document under the document profile (§4), or refuse it whole
(ValueError): a number whose nearest binary64 is infinite, NaN or Infinity (not
JSON), an unpaired surrogate, a member name repeated in one object (RFC 7493
§2.3), or nesting past 64. `-0` is read as a float."""
try:
v = json.load(fp, parse_int=_int, parse_float=_float, parse_constant=_constant,
object_pairs_hook=_no_duplicates)
except OverflowError as e:
raise ValueError("number out of range") from e
except RecursionError as e:
raise ValueError("nested deeper than %d" % MAX_DEPTH) from e
if _depth(v) > MAX_DEPTH:
raise ValueError("nested deeper than %d" % MAX_DEPTH)
if _surrogate(v):
raise ValueError("unpaired surrogate")
return v
# A decimal string (§4): JSON's number grammar in ASCII -- no sign but `-`, no
# whitespace, no bare point, no leading zero -- with at most 34 significand digits and
# a one-to-three-digit exponent, so every one is exact in IEEE 754 decimal128.
DECIMAL = re.compile(r"-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]{1,3})?", re.ASCII)
def dec(v):
"""A quantity a check reads: a decimal string, or an integer in I-JSON's range."""
if isinstance(v, str) and DECIMAL.fullmatch(v) and len(re.sub(r"[eE].*|[-.]", "", v)) <= 34:
return Decimal(v)
if isinstance(v, int) and not isinstance(v, bool) and -MAX_SAFE <= v <= MAX_SAFE:
return Decimal(v)
raise TypeError("decimal")
# Every composition check is exact (§6): no operation here may round. A check that
# ever did would trap Inexact rather than compare rounding noise.
EXACT = Context(prec=MAX_PREC, Emax=MAX_EMAX, Emin=MIN_EMIN,
traps=[InvalidOperation, DivisionByZero, Overflow, Inexact])
def _forbidden_number(v):
if isinstance(v, dict):
return any(_forbidden_number(x) for x in v.values())
if isinstance(v, list):
return any(_forbidden_number(x) for x in v)
if isinstance(v, bool):
return False
if isinstance(v, float):
return True
return isinstance(v, int) and not -MAX_SAFE <= v <= MAX_SAFE
def h(alg, data):
if alg == "sha-256":
return "sha256:" + hashlib.sha256(data).hexdigest()
if alg == "blake3":
try:
import blake3
return "blake3:" + blake3.blake3(data).hexdigest()
except ImportError:
raise KeyError("missing_capability: blake3")
raise KeyError("missing_capability: " + alg)
# Ed25519 key decoding, strictly (RFC 8032 §5.1.3), with the RFC's own reference
# arithmetic (§6). OpenSSL accepts a key that is a non-canonical encoding, or a point
# of small order -- which verifies a signature over any message -- so the reference
# checks both before it asks OpenSSL anything.
_P = 2**255 - 19
_D = -121665 * pow(121666, _P - 2, _P) % _P
_SQRT_M1 = pow(2, (_P - 1) // 4, _P)
def _point_add(p, q):
a, b = (p[1] - p[0]) * (q[1] - q[0]) % _P, (p[1] + p[0]) * (q[1] + q[0]) % _P
c, d = 2 * p[3] * q[3] * _D % _P, 2 * p[2] * q[2] % _P
e, f, g, h = b - a, d - c, d + c, b + a
return (e * f % _P, g * h % _P, f * g % _P, e * h % _P)
def _point_mul(s, p):
q = (0, 1, 1, 0)
while s > 0:
if s & 1:
q = _point_add(q, p)
p = _point_add(p, p)
s >>= 1
return q
def _decompress(b):
"""The point `b` encodes, or None when it is not a canonical encoding of one."""
y = int.from_bytes(b, "little")
sign, y = y >> 255, y & ((1 << 255) - 1)
if y >= _P:
return None
x2 = (y * y - 1) * pow(_D * y * y + 1, _P - 2, _P) % _P
if x2 == 0:
if sign:
return None
x = 0
else:
x = pow(x2, (_P + 3) // 8, _P)
if (x * x - x2) % _P:
x = x * _SQRT_M1 % _P
if (x * x - x2) % _P:
return None
if (x & 1) != sign:
x = _P - x
return (x, y, 1, x * y % _P)
def strict_key(b):
"""A published Ed25519 key a signature can rest on: 32 bytes, the canonical
encoding of a point, and a point not of small order."""
a = _decompress(b) if len(b) == 32 else None
if a is None:
return False
x, y, z, _ = _point_mul(8, a)
return not (x % _P == 0 and (y - z) % _P == 0) # [8]A is not the identity
def verify_signature(receipt, keys):
if receipt.get("sig_alg") != "ed25519":
return "missing_capability: " + str(receipt.get("sig_alg"))
signer = receipt["chain"]["signer"]
pub = keys.get(signer) if isinstance(signer, str) else None
if not isinstance(pub, str):
return "signer_unknown"
try:
from cryptography.hazmat.primitives.asymmetric import ed25519
if not strict_key(bytes.fromhex(pub)):
return "signature_invalid"
ed25519.Ed25519PublicKey.from_public_bytes(bytes.fromhex(pub)).verify(bytes.fromhex(receipt["chain"]["signature"]), receipt["chain"]["receipt_hash"].encode())
return None
except Exception:
return "signature_invalid"
def verify_receipt(r, keys):
"""Returns a list of failure reasons (empty means valid).
A missing or mistyped member is reported as `malformed: <member>` rather than
raised: every failure is an explicit reason, including a receipt too broken to
check. wai-rs's `oer` verifier and this one agree on that.
"""
try:
with localcontext(EXACT):
return _verify_receipt(r, keys)
except (KeyError, TypeError, AttributeError, InvalidOperation) as e:
return ["malformed: " + str(e.args[0] if e.args else type(e).__name__)]
def _verify_receipt(r, keys):
reasons = []
if not isinstance(r, dict):
return ["malformed: receipt"]
# The major comes from the string only: a parsed float would make it depend on how
# each JSON parser rounds (2.99999999999999977 is 3.0 to one and not the other).
o = r.get("oer")
if not isinstance(o, str) or o.split(".")[0] != "2":
return ["unknown_major"]
if not (isinstance(r.get("kind"), str) and r["kind"] in KINDS):
reasons.append("unknown_kind")
try:
body = canonical_bytes(r)
if "chain" in r:
_order(r["chain"], "chain") # outside the body, but its members are fixed too (§3)
except ValueError as e:
return ["canonical_failure: " + str(e)]
if _forbidden_number({k: v for k, v in r.items() if k not in ("chain", "receipt_id")}):
return ["malformed: number"]
try:
if not isinstance(r["hash_alg"], str):
raise TypeError("hash_alg")
if h(r["hash_alg"], body) != r["receipt_id"]:
reasons.append("receipt_id_mismatch")
expected = h(r["hash_alg"], r["chain"]["prev_hash"].encode() + body)
if expected != r["chain"]["receipt_hash"]:
reasons.append("receipt_hash_mismatch")
except KeyError as e:
# e.args[0], not str(e): str() of a KeyError is repr-quoted, which turned
# 'missing_capability' into "'missing_capability" -- a reason no vector could name.
# An absent member is not a capability gap; let it surface as `malformed`.
if str(e.args[0]).startswith("missing_capability"):
return [e.args[0]]
raise
e = r["energy"]
unmetered = is_unmetered(r)
if unmetered:
# The marker states no figure, so nothing sums to it: every term is null, the
# grade is the weakest, no class names how a figure was read, and no bundle's
# stages sum to it (§3.1).
if any(v is not None for v in e["breakdown"].values()) or r["provenance"]["how"] != "projected" \
or "class" in e or r.get("kind") == "bundle":
reasons.append("unmetered_invalid")
else:
total = sum(dec(v) for v in e["breakdown"].values())
if abs(total - dec(e["joules_total"])) > Decimal("1e-9") * max(Decimal(1), abs(total)):
reasons.append("sum_mismatch")
b = e.get("boundary")
if not (isinstance(b, str) and b):
reasons.append("boundary_missing")
f = r["floor"]
floor = dec(f["bits_committed"]) * KBOLTZ * dec(e["temperature_k"]) * LN2
if abs(floor - dec(f["floor_j"])) > abs(floor) * Decimal("0.01"):
reasons.append("floor_mismatch")
if r["provenance"]["how"] not in GRADES:
reasons.append("grade_unknown")
if r.get("oer") == MINOR_1 and "class" in e and not unmetered:
c = e["class"]
if not class_valid(c):
reasons.append("class_invalid")
elif r["provenance"]["how"] in GRADES and CLASS_GRADE[c["label"]] != r["provenance"]["how"]:
reasons.append("class_grade_mismatch")
# An abstention makes no verdict claim: no answer, and no record anything could
# later be served from (§3). Read leniently, so a strange shape is simply not an
# abstention rather than a different reason.
w = r.get("work")
if isinstance(w, dict) and w.get("outcome") == "abstain":
sub = r.get("subject") if isinstance(r.get("subject"), dict) else {}
st = r.get("settlement") if isinstance(r.get("settlement"), dict) else {}
settled = any(st.get(k) for k in ("served_from_record", "record_id", "record_hash"))
if sub.get("output") or settled:
reasons.append("abstention_settled")
if r.get("kind") == "bundle":
stages = r["bundle"]["stages"]
if not isinstance(stages, list):
raise TypeError("stages")
root = h(r["hash_alg"], stage_list_bytes(stages))
if root != r["bundle"]["provenance_root"]:
reasons.append("bundle_root_mismatch")
st = sum(dec(s["joules"]) for s in stages)
if not unmetered and abs(st - dec(e["joules_total"])) > Decimal("1e-9") * max(Decimal(1), abs(st)):
reasons.append("bundle_sum_mismatch")
eff = r.get("efficiency")
if not isinstance(eff, dict) or eff.get("completed_units") is None:
reasons.append("efficiency_missing")
sig = verify_signature(r, keys)
if sig:
reasons.append(sig)
return reasons
def is_unmetered(r):
"""Whether a receipt states the unmetered marker (§3.1): minor version 1 and a null
`energy.joules_total`. A verifier reports it as unmetered, never as a figure."""
e = r.get("energy") if isinstance(r, dict) and r.get("oer") == MINOR_1 else None
return isinstance(e, dict) and "joules_total" in e and e["joules_total"] is None
def _uint(v, top):
return isinstance(v, int) and not isinstance(v, bool) and 1 <= v <= top
def class_valid(c):
"""`energy.class` as §3.1 states it: a label of the four classes a new profile may name
(never the legacy HwShunt), and exactly that label's evidence."""
if not isinstance(c, dict) or not isinstance(c.get("label"), str) or c["label"] not in CLASS_MEMBERS:
return False
if set(c) != CLASS_MEMBERS[c["label"]]:
return False
if c["label"] in ("ModelBased", "Estimator"):
return True
if not (_uint(c["relative_ppm"], 2**32 - 1) and _uint(c["window_us"], MAX_SAFE)):
return False
if c["label"] == "OnChipCounter":
return isinstance(c["filtering"], str) and c["filtering"] in FILTERING
return isinstance(c["calibration_ref"], str) and c["calibration_ref"] != ""
def unmetered_ids(receipts):
"""The receipts a verifier reports as unmetered, by receipt id, in order."""
return [r.get("receipt_id") if isinstance(r.get("receipt_id"), str) else "?" for r in receipts if is_unmetered(r)]
def _link(r, k):
ch = r.get("chain") if isinstance(r, dict) else None
v = ch.get(k) if isinstance(ch, dict) else None
return v if isinstance(v, str) else None
def verify_chain(receipts, keys):
reasons = {}
for i, r in enumerate(receipts):
rr = verify_receipt(r, keys)
# Every link after the first: a string equal to the previous receipt's hash,
# which must itself be a string -- a hash that is not one links nothing.
if i > 0:
prev = _link(receipts[i - 1], "receipt_hash")
if prev is None or _link(r, "prev_hash") != prev:
rr.append("chain_break")
if rr:
# Accumulate: a receipt id that recurs must not replace, and so hide, the
# reasons an earlier receipt with the same id already failed for.
rid = r.get("receipt_id", "?") if isinstance(r, dict) else "?"
rid = rid if isinstance(rid, str) else json.dumps(rid, separators=(",", ":"), ensure_ascii=False)
reasons.setdefault(rid, []).extend(rr)
return reasons
def record_hash(rec):
"""A record whose key or answer is not a string, or whose created is not a
number, cannot be rehashed (§6)."""
key, answer, created = rec["key"], rec["answer"], rec["created"]
if not (isinstance(key, str) and isinstance(answer, str)) or isinstance(created, bool) \
or not isinstance(created, (int, float)):
raise TypeError("record")
return "sha256:" + hashlib.sha256(("%s|%s|%.1f" % (key, answer, created)).encode()).hexdigest()
def verify_records(receipts, records):
bad = []
records = records if isinstance(records, dict) else {}
for r in receipts:
s = r.get("settlement") if isinstance(r, dict) else None
if not isinstance(s, dict) or not s.get("served_from_record") or s.get("record_id") == "unverified":
continue
rid = s.get("record_id")
rec = records.get(rid) if isinstance(rid, str) else None # a non-string id names no record
try:
ok = isinstance(rec, dict) and record_hash(rec) == s.get("record_hash")
except (KeyError, TypeError, ValueError):
ok = False # a record too broken to rehash cannot vouch for anything
if not ok:
bad.append(str(r.get("receipt_id")))
return bad
def verify_ledger(receipts, ledger):
try:
with localcontext(EXACT):
return _verify_ledger(receipts, ledger)
except (KeyError, TypeError, AttributeError, InvalidOperation, IndexError):
return False # an identity that cannot be summed does not hold
def _verify_ledger(receipts, ledger):
total = sum(dec(r["energy"]["joules_total"]) for r in receipts) + dec(ledger["unallocated_joules"]) + dec(ledger.get("carried_forward_joules", "0"))
meter = dec(ledger["meter_joules"])
return abs(total - meter) <= Decimal("1e-9") * max(Decimal(1), abs(meter))
# The canonical form, pinned independently of the code that produces it.
#
# vectors.json cannot check this on its own: generate_vectors.py imports this module
# for canonical_bytes/_order/h, so a defect in the field order is baked into every
# receipt id and then blessed by this verifier. Reversing the declared order and
# regenerating still yields nine passes, with different receipt ids.
#
# The fixture below closes that. Its input deliberately supplies keys in the WRONG
# order at two levels, so the ordering is proved rather than incidental, and the
# expected output is a literal — the one thing in this file that does not come from
# _order. If canonicalization changes, this fails and the vectors do not have to.
CANONICAL_FIXTURE = {
"sig_alg": "ed25519", "kind": "decision", "oer": "2.0",
"capability": "oer.canonical.fixture", "intent": "replicate",
"energy": {
"method": "fixture",
"breakdown": {"upstream_j": 5, "compute_j": 1, "idle_alloc_j": 3,
"movement_j": 2, "shared_alloc_j": 4},
"joules_total": 15, "boundary": "node",
},
"hash_alg": "sha-256",
}
CANONICAL_FIXTURE_BYTES = "{\"oer\":\"2.0\",\"kind\":\"decision\",\"capability\":\"oer.canonical.fixture\",\"intent\":\"replicate\",\"energy\":{\"joules_total\":15,\"boundary\":\"node\",\"breakdown\":{\"compute_j\":1,\"movement_j\":2,\"idle_alloc_j\":3,\"shared_alloc_j\":4,\"upstream_j\":5},\"method\":\"fixture\"},\"hash_alg\":\"sha-256\",\"sig_alg\":\"ed25519\"}"
CANONICAL_FIXTURE_ID = "sha256:364718c762d6de8afd6ad59dcc5023eafbf0023b6bc4acb9f4885de17084ed30"
# The stage list `bundle.provenance_root` addresses, pinned the same way. Every
# stage list in the corpus is ASCII, and on ASCII the escaped and unescaped
# serialisations agree, so no vector can tell them apart; this input is non-ASCII
# (a two-byte and a four-byte character) with its members out of order, against a
# literal.
STAGE_FIXTURE = [{"joules": "2.5", "receipt_hash": "sha256:00", "kind": "décision 😀"}]
STAGE_FIXTURE_BYTES = '[{"kind":"décision 😀","receipt_hash":"sha256:00","joules":"2.5"}]'
STAGE_FIXTURE_ID = "sha256:c1caa0cea7cf581f240346d9e9431d19cbe204c4daca157fea555fee6bf2638b"
# Minor version 1's members (§3.1), pinned the same way: `energy.class` and its members
# out of order, against a literal; and the same input under "2.0", which §3 refuses.
CLASS_FIXTURE = {
"sig_alg": "ed25519", "hash_alg": "sha-256", "oer": "2.1", "capability": "oer.job.media.encode", "kind": "job",
"energy": {"class": {"window_us": 1000000, "filtering": "Off", "relative_ppm": 20000, "label": "OnChipCounter"},
"boundary": "hop", "joules_total": "1.5"},
}
CLASS_FIXTURE_BYTES = '{"oer":"2.1","kind":"job","capability":"oer.job.media.encode","energy":{"joules_total":"1.5","boundary":"hop","class":{"label":"OnChipCounter","relative_ppm":20000,"window_us":1000000,"filtering":"Off"}},"hash_alg":"sha-256","sig_alg":"ed25519"}'
CLASS_FIXTURE_ID = "sha256:b473c94d166e9a262c4a151db7cad35f8e16b15b862d5bcfaa88750c83388b94"
def check_canonical_form():
"""Return a list of reasons the canonical form or the stage list no longer
matches its fixture."""
reasons = []
got = canonical_bytes(CANONICAL_FIXTURE).decode("utf-8")
if got != CANONICAL_FIXTURE_BYTES:
reasons.append("canonical_form_mismatch")
if h("sha-256", got.encode("utf-8")) != CANONICAL_FIXTURE_ID:
reasons.append("canonical_id_mismatch")
got = stage_list_bytes(STAGE_FIXTURE).decode("utf-8")
if got != STAGE_FIXTURE_BYTES:
reasons.append("stage_form_mismatch")
if h("sha-256", got.encode("utf-8")) != STAGE_FIXTURE_ID:
reasons.append("stage_id_mismatch")
got = canonical_bytes(CLASS_FIXTURE).decode("utf-8")
if got != CLASS_FIXTURE_BYTES:
reasons.append("class_form_mismatch")
if h("sha-256", got.encode("utf-8")) != CLASS_FIXTURE_ID:
reasons.append("class_id_mismatch")
try:
canonical_bytes(dict(CLASS_FIXTURE, oer="2.0"))
reasons.append("class_admitted_in_2_0")
except ValueError:
pass
# The number rule reads -0 through load(); if that ever stops yielding a float,
# -0 would read as the integer zero and verify. Fail here, loudly, instead.
if not _forbidden_number(load(io.StringIO("-0"))):
reasons.append("number_profile_unavailable")
return reasons
def run(vectors):
keys = vectors.get("keys") if isinstance(vectors, dict) else None
if not isinstance(keys, dict):
raise ValueError("vector file has no keys")
if not isinstance(vectors.get("vectors"), list):
raise ValueError("vector file has no vectors")
results = []
for v in vectors["vectors"]:
v = v if isinstance(v, dict) else {}
receipts = v.get("receipts") if isinstance(v.get("receipts"), list) else []
got = set()
chain = verify_chain(receipts, keys)
for rs in chain.values():
for x in rs:
got.add(x.split(":")[0])
if "records" in v and verify_records(receipts, v["records"]):
got.add("record_mismatch")
if "ledger" in v and not verify_ledger(receipts, v["ledger"]):
got.add("ledger_mismatch")
exp = v.get("expected")
expected = {x for x in exp if isinstance(x, str)} if isinstance(exp, list) else set()
res = {"vector": str(v.get("name")), "expected": sorted(expected), "got": sorted(got), "pass": got == expected}
# A vector that declares its unmetered receipts (§7) is held to them too: the
# marker is reported as unmetered, never as a figure.
if "unmetered" in v:
um = v["unmetered"]
res["unmetered_expected"] = [x for x in um if isinstance(x, str)] if isinstance(um, list) else []
res["unmetered_got"] = unmetered_ids([r for r in receipts if isinstance(r, dict)])
res["pass"] = res["pass"] and res["unmetered_got"] == res["unmetered_expected"]
results.append(res)
return results
def refuse_all(directory):
"""Every document in `directory` must be refused whole (§4)."""
import os
names = sorted(n for n in os.listdir(directory)
if n.endswith(".json") and os.path.isfile(os.path.join(directory, n)))
ok = bool(names)
for name in names:
try:
with open(os.path.join(directory, name), encoding="utf-8") as fp:
load(fp)
print("FAIL %s was read; it must be refused whole" % name)
ok = False
except ValueError as e:
print("PASS %s refused (%s)" % (name, e))
except OSError as e:
print("FAIL %s: %s" % (name, e))
ok = False
return ok
if __name__ == "__main__":
if sys.argv[1:2] == ["--unreadable"]:
sys.exit(0 if refuse_all(sys.argv[2] if len(sys.argv) > 2 else "unreadable") else 1)
path = sys.argv[1] if len(sys.argv) > 1 else "vectors.json"
try:
vec = load(open(path, encoding="utf-8"))
run(vec) # a file without keys or vectors is refused here, not mid-report
except ValueError as e:
print("FAIL %s: %s" % (path, e))
sys.exit(1)
ok = True
# The canonical form first: if it has moved, every id below is computed under a
# different rule and the passes that follow would mean nothing.
cf = check_canonical_form()
print(("PASS " if not cf else "FAIL ") + "canonical form and stage list pinned to their fixtures"
+ " expected= got=" + ",".join(cf))
ok = ok and not cf
for res in run(vec):
ok = ok and res["pass"]
um = (" unmetered expected=%s got=%s" % (",".join(res["unmetered_expected"]), ",".join(res["unmetered_got"]))) if "unmetered_got" in res else ""
print(("PASS " if res["pass"] else "FAIL ") + res["vector"] + " expected=" + ",".join(res["expected"]) + " got=" + ",".join(res["got"]) + um)
sys.exit(0 if ok else 1)