"""verifyctl — evidence-archive verifier for Wannabe on-chain evidence. Built as the deliverable of arena task genesis-verifyctl-001: the hidden test suite's SHA-256 was committed on XRPL testnet BEFORE this file was written (commit tx 272A9CB4EEB0EEE9331854002B383FCB6ABCBEAC50DBBE4B7F610824FA5B8739), and the build was settled through escrow by the deterministic referee. Checks an archive of XRPL transaction JSONs (as produced in evidence/tx_archive/): per-file integrity against MANIFEST.sha256, filename==tx hash, tesSUCCESS, validated. Stdlib only, fully offline. Usage: python verifyctl.py Exit codes: 0 = archive clean, 2 = problems found. """ import hashlib import json import os import re import sys _HASH_NAME = re.compile(r"^[0-9A-Fa-f]{64}\.json$") def _tx_fields(obj): """Return (hash, transaction_result, validated) from a tx JSON of either shape: {"result": {...}} or the bare result object.""" res = obj.get("result", obj) if isinstance(obj, dict) else {} if not isinstance(res, dict): res = {} tx_hash = res.get("hash") or (obj.get("hash") if isinstance(obj, dict) else None) meta = res.get("meta") or (obj.get("meta") if isinstance(obj, dict) else None) or {} result_code = meta.get("TransactionResult") if isinstance(meta, dict) else None validated = res.get("validated") if validated is None and isinstance(obj, dict): validated = obj.get("validated") return tx_hash, result_code, validated def _read_manifest(path): """Parse MANIFEST.sha256 -> {filename: sha256}. Lines: ' '.""" entries = {} with open(path) as f: for line in f: line = line.strip() if not line: continue parts = line.split(None, 1) if len(parts) == 2: digest, name = parts entries[name.strip()] = digest.lower() return entries def verify(archive_dir): problems = [] json_files = sorted( fn for fn in os.listdir(archive_dir) if fn.lower().endswith(".json") ) per_file_problems = {fn: [] for fn in json_files} manifest_path = os.path.join(archive_dir, "MANIFEST.sha256") manifest = None if os.path.exists(manifest_path): manifest = _read_manifest(manifest_path) else: problems.append({"file": "MANIFEST.sha256", "error": "manifest_missing"}) for fn in json_files: path = os.path.join(archive_dir, fn) raw = open(path, "rb").read() if manifest is None: per_file_problems[fn].append("unlisted") elif fn not in manifest: per_file_problems[fn].append("unlisted") elif hashlib.sha256(raw).hexdigest() != manifest[fn]: per_file_problems[fn].append("sha256_mismatch") try: obj = json.loads(raw.decode("utf-8")) except (ValueError, UnicodeDecodeError): per_file_problems[fn].append("bad_json") continue tx_hash, result_code, validated = _tx_fields(obj) stem = fn[:-5] if _HASH_NAME.match(fn): if not tx_hash or tx_hash.upper() != stem.upper(): per_file_problems[fn].append("hash_mismatch") if result_code != "tesSUCCESS": per_file_problems[fn].append("not_success") if validated is not True: per_file_problems[fn].append("not_validated") if manifest is not None: for name in sorted(manifest): if name.lower().endswith(".json") and name not in per_file_problems: problems.append({"file": name, "error": "listed_missing"}) passed = failed = 0 for fn in json_files: errs = per_file_problems[fn] if errs: failed += 1 problems.extend({"file": fn, "error": e} for e in errs) else: passed += 1 return { "checked": len(json_files), "passed": passed, "failed": failed, "problems": problems, } def main(argv): if len(argv) != 2: print(json.dumps({"error": "usage: verifyctl.py "})) return 2 report = verify(argv[1]) print(json.dumps(report, sort_keys=True, indent=1)) return 0 if report["failed"] == 0 else 2 if __name__ == "__main__": sys.exit(main(sys.argv))