open-standards/wai/oer-conformance/fuzz_differential.py
Transaction Science 54e89b5638
Some checks failed
no-vanity-metrics / Scan committed Markdown for vanity metrics (push) Successful in 3s
standards workspaces build + test / eoc-rs (push) Successful in 1m50s
standards workspaces build + test / jcp-rs (push) Successful in 1m17s
standards workspaces build + test / proof-rs (push) Successful in 1m12s
standards workspaces build + test / sandbox-rs (push) Successful in 2m6s
standards workspaces build + test / smart-byte-rs (push) Successful in 2m29s
WAI conformance / cargo-class corpora · linux (push) Successful in 1m10s
WAI conformance / cross-arch byte-equality · linux (push) Successful in 14s
WAI conformance / C2PA manifest emission · signed, read back, validated (push) Successful in 40s
standards workspaces build + test / jouleclaw-rs (push) Successful in 3m31s
standards workspaces build + test / aac-rs (push) Successful in 1m28s
standards workspaces build + test / joule-code-rs (push) Successful in 1m3s
standards workspaces build + test / joulecontract-rs (push) Successful in 1m14s
standards workspaces build + test / map-rs (push) Successful in 55s
standards workspaces build + test / mesh-rs (push) Successful in 1m12s
standards workspaces build + test / openpay (push) Successful in 3m52s
WAI conformance / cargo-class corpora · linux-arm64 (push) Successful in 2m9s
WAI conformance / cross-arch byte-equality · linux-arm64 (push) Successful in 1m25s
standards workspaces build + test / arl-rs (push) Successful in 16m28s
standards workspaces build + test / joule-ui-rs (push) Successful in 8m32s
standards workspaces build + test / jouledesk-rs (push) Successful in 3m57s
standards workspaces build + test / joulehook-rs (push) Successful in 3m25s
standards workspaces build + test / sense-rs (push) Successful in 3m27s
standards workspaces build + test / macOS energy backend (jcp-energy) (push) Successful in 3m23s
WAI conformance / cargo-class corpora · macos (push) Successful in 1m0s
WAI conformance / cross-arch byte-equality · macos (push) Successful in 14s
WAI conformance / cargo-class corpora · windows (push) Has been cancelled
WAI conformance / cross-arch byte-equality · windows (push) Has been cancelled
wai/oer: differential fuzzing of the reference verifier against wai-rs
The vectors pin reason-set-equivalence one case at a time; fuzz_differential.py checks
the space between them. It mutates real corpus receipts, chains, records, ledgers,
published keys and signatures (type swaps at any member, deletions, unknown members,
respelled decimals, repeated member names, literals no JSON library writes) and
requires both verifiers to report the same reasons or the same refusal for every
document, with no crash in either. wai_oer_diff is the Rust half.

80k documents across four seeds: no divergence, no crash. A divergence planted in
wai-rs (the old truthiness rule for energy.boundary) is caught and fails the run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 01:27:31 -04:00

218 lines
9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Differential fuzzing of the two OER verifiers.
Conformance is reason-set-equivalence, and the corpus pins it vector by vector. This
checks the rest of the input space: it mutates real corpus receipts, chains, records,
ledgers, published keys and signatures -- type swaps at any member, deletions, unknown
members, respelled decimals, repeated member names, literals no JSON library writes
(`-0`, `1e2`, lone surrogates) -- and requires the reference verifier here and wai-rs
(`wai_oer_diff`) to report the same thing for every document: the same reasons, the
same refusal, and no crash in either.
python3 fuzz_differential.py [--seed N] [--cases N]
Exit status 0 when every document agrees. Deterministic for a given seed. Needs
`cryptography`; receipts hashed with blake3 are included only when the `blake3` module
is installed -- without it the reference rightly reports `missing_capability` for them
(extension section 5) where wai-rs, which has blake3, goes on to check the hash.
"""
import argparse, copy, hashlib, importlib.util, io, json, os, random, subprocess, sys, tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import oer2_verify as V # noqa: E402
from cryptography.hazmat.primitives.asymmetric import ed25519 # noqa: E402
HAVE_BLAKE3 = importlib.util.find_spec("blake3") is not None
class Lit:
"""Raw JSON text, written verbatim."""
def __init__(self, text):
self.text = text
class Pairs:
"""An object that may repeat a member name."""
def __init__(self, pairs):
self.pairs = pairs
def enc(v):
if isinstance(v, Lit):
return v.text
if isinstance(v, Pairs):
return "{" + ",".join(json.dumps(k) + ":" + enc(x) for k, x in v.pairs) + "}"
if isinstance(v, dict):
return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + enc(x) for k, x in v.items()) + "}"
if isinstance(v, list):
return "[" + ",".join(enc(x) for x in v) + "]"
return json.dumps(v, ensure_ascii=False)
BASE = json.load(open(os.path.join(HERE, "vectors.json")))
KEYS = BASE["keys"]
V01 = BASE["vectors"][0]
R = V01["receipts"][0]
BUNDLE = next(r for v in BASE["vectors"] for r in v["receipts"] if r.get("kind") == "bundle")
PRIV = ed25519.Ed25519PrivateKey.from_private_bytes(
hashlib.sha256(b"settlement world node ledger vector key").digest())
VALUES = [None, True, False, 0, 1, -1, 5, 2**53 - 1, 2**53, -(2**53), 2**64, "", "x", "0", "1.5", "+1",
" 1", "1.", "01", "1e1000", "١", "NaN", "abstain", "escalate", "2.0", "3.0", "sha-256",
"sha3-512", "ed25519", [], {}, [0.5], {"a": 1}, [1, [2, [3]]], 0.5, 1e300,
Lit("-0"), Lit("1e2"), Lit("1.0"), Lit("-0.0"), Lit("2.99999999999999977"), Lit("9007199254740993"),
Lit('"\\u0000"'), Lit('"\\ud83d\\ude00"'), Lit('"\\ud800"'), Lit("1.7976931348623157e308"),
Lit("123456789012345678901234567890")]
if HAVE_BLAKE3:
VALUES.append("blake3")
def reseal(r):
"""Re-seal after an edit where the body still canonicalises, so the checks past the
seal are reached; otherwise leave the stale seal, which is itself a case."""
try:
body = V.canonical_bytes(r)
r["receipt_id"] = V.h(r["hash_alg"], body)
r["chain"]["receipt_hash"] = V.h(r["hash_alg"], r["chain"]["prev_hash"].encode() + body)
r["chain"]["signature"] = PRIV.sign(r["chain"]["receipt_hash"].encode()).hex()
except Exception:
pass
return r
def paths(v, p=()):
yield p
if isinstance(v, dict):
for k, x in v.items():
yield from paths(x, p + (k,))
elif isinstance(v, list):
for i, x in enumerate(v):
yield from paths(x, p + (i,))
def at(v, p):
for k in p:
v = v[k]
return v
def mutate(rng, r, n):
for _ in range(n):
choices = [p for p in paths(r) if p]
if not choices:
break
p = rng.choice(choices)
parent, k = at(r, p[:-1]), p[-1]
op = rng.random()
if op < 0.55:
parent[k] = copy.deepcopy(rng.choice(VALUES))
elif op < 0.7 and isinstance(parent, dict):
del parent[k]
elif op < 0.8 and isinstance(parent, dict):
parent[rng.choice(["extra", "zz", str(k) + "_x"])] = copy.deepcopy(rng.choice(VALUES))
elif op < 0.9 and isinstance(parent[k], str):
s = parent[k]
parent[k] = rng.choice([s + "0", "+" + s, s.upper(), s[:-1], s + " ", s.replace(".", ","), s * 2])
elif isinstance(parent, dict): # a repeated member name
items = list(parent.items())
items.insert(rng.randrange(len(items) + 1), (k, copy.deepcopy(rng.choice(VALUES))))
if len(p) == 1:
return Pairs(items)
at(r, p[:-2])[p[-2]] = Pairs(items)
break # the tree below is no longer plain dicts
return r
def document(receipts, extra=None, keys=None):
vec = {"name": "c", "receipts": receipts, "expected": []}
vec.update(extra or {})
return enc({"keys": KEYS if keys is None else keys, "vectors": [vec]})
def case(rng):
kind = rng.random()
if kind < 0.6: # one receipt or bundle, mutated, re-sealed half the time
r = mutate(rng, copy.deepcopy(rng.choice([R, BUNDLE])), rng.randint(1, 3))
if rng.random() < 0.5 and not isinstance(r, Pairs):
r = reseal(r)
return document([r])
if kind < 0.7: # a two-receipt chain with a mutated link
a, b = copy.deepcopy(R), copy.deepcopy(R)
b["chain"]["prev_hash"] = a["chain"]["receipt_hash"]
reseal(b)
mutate(rng, rng.choice([a, b])["chain"], 1)
return document([a, b])
if kind < 0.8: # records
recs = copy.deepcopy(V01["records"])
rid = R["settlement"]["record_id"]
if rng.random() < 0.5:
recs = rng.choice([[], None, "x", {rid: copy.deepcopy(rng.choice(VALUES))}])
else:
mutate(rng, recs[rid], 1)
return document([copy.deepcopy(R)], {"records": recs})
if kind < 0.85: # ledgers
ledger = copy.deepcopy(V01["ledger"])
mutate(rng, ledger, rng.randint(1, 2))
return document(copy.deepcopy(V01["receipts"][:20]), {"ledger": ledger})
if kind < 0.93: # published keys
keys = copy.deepcopy(KEYS)
name = next(iter(keys))
keys[name] = rng.choice([None, 5, "", "zz", keys[name][:-2], keys[name] + "00", keys[name].upper(),
" " + keys[name], "01" + "00" * 31, "00" * 32, "ec" + "ff" * 30 + "7f"])
return document([copy.deepcopy(R)], keys=keys)
r = copy.deepcopy(R) # signature bytes
sig = bytearray(bytes.fromhex(r["chain"]["signature"]))
sig[rng.randrange(64)] ^= 1 << rng.randrange(8)
r["chain"]["signature"] = rng.choice([sig.hex(), sig.hex().upper(), sig.hex()[:-2], "", " " + sig.hex()])
return document([r])
def reference(text):
try:
doc = V.load(io.StringIO(text))
except ValueError:
return "REFUSED"
except Exception as e: # any other exception is a crash the fuzzer must report
return "CRASH:load:" + type(e).__name__
try:
results = V.run(doc)
except ValueError:
return "NOFILE"
except Exception as e:
return "CRASH:run:" + type(e).__name__
return " | ".join("%s=%s" % (x["vector"], ",".join(x["got"])) for x in results)
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("--seed", type=int, default=1)
ap.add_argument("--cases", type=int, default=20000)
args = ap.parse_args()
rng = random.Random(args.seed)
texts = [case(rng) for _ in range(args.cases)]
ours = [reference(t) for t in texts]
with tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False) as f:
for t in texts:
f.write(json.dumps(t) + "\n")
rust = subprocess.run(
["cargo", "run", "-q", "--release", "--no-default-features", "--features", "oer",
"--bin", "wai_oer_diff", "--", f.name],
cwd=os.path.join(HERE, "..", "wai-rs"), capture_output=True, text=True, check=True).stdout.splitlines()
os.unlink(f.name)
assert len(rust) == len(ours), (len(rust), len(ours))
diverged = [i for i in range(len(ours)) if ours[i] != rust[i]]
crashes = sum(o.startswith("CRASH") for o in ours) + rust.count("CRASH")
shape = {k: sum(1 for o in ours if f(o)) for k, f in [
("refused", lambda o: o == "REFUSED"), ("valid", lambda o: o.endswith("=")),
("with reasons", lambda o: o not in ("REFUSED", "NOFILE") and not o.endswith("=") and "CRASH" not in o)]}
print("seed %d: %d documents (%s)%s" % (args.seed, len(ours), ", ".join("%d %s" % (n, k) for k, n in shape.items()),
"" if HAVE_BLAKE3 else "; blake3 receipts left out (no blake3 module)"))
for i in diverged[:20]:
print("DIVERGED %d\n reference: %s\n wai-rs: %s\n document: %s" % (i, ours[i], rust[i], texts[i][:400]))
print("%d diverged, %d crashed" % (len(diverged), crashes))
sys.exit(1 if diverged or crashes else 0)
if __name__ == "__main__":
main()