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]>
597 lines
28 KiB
Python
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)
|