#!/usr/bin/env python3 """Standalone proof verifier for Conviction Agent. No deps for the chain check. Usage: curl -s https:///proof/export > proof.json && python verify.py proof.json For the Bitcoin timestamp, also: pip install opentimestamps-client, then `ots verify` the .ots written per anchor below. This script trusts NOTHING from the server beyond the exported JSON. """ import sys, json, hashlib, base64, os def canon(d): return json.dumps({k: v for k, v in d.items() if k != "hash"}, sort_keys=True, separators=(",", ":"), ensure_ascii=False) def sha(s): return hashlib.sha256(s.encode("utf-8")).hexdigest() def check_chain(name, entries): prev = "genesis"; bad = [] for e in entries: body = {k: v for k, v in e.items() if k != "hash"} if e.get("prev") != prev or e.get("hash") != sha(canon(body)): bad.append(e.get("i", e.get("id"))) prev = e.get("hash", prev) print(f" {name}: {len(entries)} entries, chain {'OK' if not bad else 'BROKEN at ' + str(bad)}, root={prev[:16]}...") return prev, not bad def check_anchors(name, entries, anchors, root): # an anchor commits (count, root). recompute the root over the first `count` entries. for a in anchors: n = a.get("count") if not isinstance(n, int) or len(entries) < n: print(f" {name}: anchor count={n} but chain shorter -> TRUNCATION"); continue prev = "genesis" for e in entries[:n]: body = {k: v for k, v in e.items() if k not in ("hash", "prev")}; body["prev"] = prev prev = sha(canon(body)) ok = (prev == a.get("root")) ots = a.get("ots_b64") if ots: open(f"{name}_anchor_{n}.ots", "wb").write(base64.b64decode(ots)) print(f" {name}: anchor@{n} {'OK' if ok else 'MISMATCH (rewrite!)'}" + (f" -> {name}_anchor_{n}.ots written (run: ots verify)" if ots else " (no OTS)")) def main(): d = json.load(open(sys.argv[1], encoding="utf-8")) for name in ("directional", "funding", "verdicts"): t = d.get(name) or {} entries = t.get("entries") or [] if not entries: print(f" {name}: empty"); continue # verdict entries are hashed over a claim subset, not a prev-link; skip strict chain there if name == "verdicts": print(f" {name}: {len(entries)} verdicts (each hash = sha256 of its claim; see /proof export)") else: root, _ = check_chain(name, entries) check_anchors(name, entries, t.get("anchors") or [], root) if __name__ == "__main__": main()