#!/usr/bin/env python3 """Standalone verifier for CAIN-42 hourly operational proofs. No CAIN imports. Needs Python 3.8+, `cryptography`, and verify_pbft_qc_bundle.py in the same directory. python3 verify_hourly_proofs.py https://clawx.click/evidence/hourly-proof/ python3 verify_hourly_proofs.py BASE --pin For every proof in the index: proof_hash recomputed from canonical JSON; Ed25519 signature by the evidence-root key (pinned from the three sites' published key unless --pin is given); the hash chain (each proof names the previous one's hash); and for every proof whose verdict is OPERATIONAL, the embedded COMMIT and PREPARE quorum certificates are re-verified against the membership (distinct members, quorum, signatures) and the replicas it lists agree. Final verdict: PROVEN OPERATIONAL (last proof N min ago) -- chain valid, latest OPERATIONAL, < 1 h old (proofs run every 30 min) NOT PROVEN FOR X h -- chain valid but stale or latest not operational FAILED -- any proof does not verify """ from __future__ import annotations import datetime import hashlib import json import os import sys import urllib.request sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import verify_pbft_qc_bundle as Q # noqa: E402 DOMAIN = "CAIN42/HOURLY-PROOF/v1" KEY_URLS = ["https://cainstudio.online/proof/bundle/claims/evidence-root.pub.json", "https://mcpgate.online/proof/bundle/claims/evidence-root.pub.json", "https://clawx.click/evidence/claims/evidence-root.pub.json"] def get(u): with urllib.request.urlopen(u, timeout=30) as r: return json.loads(r.read()) def canon(o) -> bytes: return json.dumps(o, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode() def main(argv): if len(argv) < 2: print(__doc__); return 2 base = argv[1].rstrip("/") + "/" pin = argv[argv.index("--pin") + 1] if "--pin" in argv else None problems = [] if pin is None: keys = set() for u in KEY_URLS: try: keys.add(get(u)["public_key_b64"]) except Exception as e: # noqa: BLE001 problems.append(f"evidence-root key unreachable at {u}: {e}") if len(keys) != 1: problems.append(f"evidence-root key differs across sites: {keys}") pin = next(iter(keys)) if keys else None idx = get(base + "index.json") mb = Q.Membership(idx["membership"]) if mb.configuration_hash != idx["configuration_hash"]: problems.append("membership does not recompute to the stated configuration hash") prev = hashlib.sha256(canon({"domain": DOMAIN, "genesis": idx["cluster_id"]})).hexdigest() operational = 0 last = None for i, e in enumerate(idx["proofs"]): p = get(base + e["file"]) b = p["body"] d = hashlib.sha256(canon(b)).hexdigest() bad = [] if d != p["proof_hash"]: bad.append("proof_hash does not match content") if not Q.ed25519_ok(pin, p["evidence_root_signature_b64"], p["proof_hash"].encode()): bad.append("evidence-root signature invalid") if b["n"] != i or b["previous_proof_hash"] != prev: bad.append("hash chain broken") if b["configuration_hash"] != mb.configuration_hash: bad.append("wrong membership") if b["verdict"] == "OPERATIONAL": w = b["write"] for k in ("commit_qc", "prepare_qc"): ok, why = Q.verify_qc(w[k], mb) if not ok: bad.append(f"{k}: {why}") if w["commit_qc"]["sequence"] != w["sequence"]: bad.append("certificate is for another sequence") up = [r for r in b["replicas"].values() if r.get("reachable")] by_h = {} for r in up: by_h.setdefault(r["commit_index"], set()).add(r["application_state_hash"]) if len(up) < 3 or any(len(v) > 1 for v in by_h.values()): bad.append("stated OPERATIONAL but replicas do not agree") operational += 1 print(f"[{'VALID' if not bad else 'INVALID'}] {e['file']} {b['at']} {b['verdict']}" + (f" seq {b['write'].get('sequence')}" if b['write'].get('committed') else "") + ("".join(f"\n - {x}" for x in bad))) problems += [f"{e['file']}: {x}" for x in bad] prev = p["proof_hash"] last = b if problems or last is None: print(f"\nFAILED: {len(problems)} problem(s)" if problems else "\nFAILED: no proofs") return 1 age = (datetime.datetime.now(datetime.timezone.utc) - datetime.datetime.strptime(last["at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc)).total_seconds() if last["verdict"] == "OPERATIONAL" and age < 3600: print(f"\nPROVEN OPERATIONAL (last proof {age / 60:.0f} min ago; {operational}/{len(idx['proofs'])} proofs operational; chain valid)") return 0 print(f"\nNOT PROVEN FOR {age / 3600:.1f} h (latest verdict {last['verdict']}; chain valid)") return 3 if __name__ == "__main__": sys.exit(main(sys.argv))