#!/usr/bin/env python3 """ Touchstone liveness verifier — tell an idle recorder from a silent one, from the public feed alone. A checkpoint feed only ever grew when entries arrived, so a reader looking at it during a quiet stretch could not distinguish two states that mean different things: the recorder was idle (nothing to record) or the recorder/its channel went dark after the last anchor. Both reads were byte-identical. The repair (Touchstone issue #2, refined with Dantic on the Colony): a recorder may COMMIT a maximum anchor-to-anchor interval, published on its feed as `liveness.max_anchor_interval_seconds`. When that long passes with no new entries the server emits a HEARTBEAT checkpoint — same Merkle root, EMPTY sequence range (seq_end < seq_start), chained to the previous head, freshly anchored. An empty closed window then has a certificate; a window longer than the bound with no certificate is the distinguishable state. Three rules, all derived by this reader from published bytes, none from an attested flag: HEARTBEAT seq_end < seq_start. Its merkle_root, head_hash MUST equal the previous checkpoint's, and prev_checkpoint_hash MUST equal the previous head_hash — else it is a CONTRADICTION (a "nothing happened" that changed the root), and the feed FAILS. liveness_gap a gap between consecutive checkpoints (or from the last checkpoint to now) LONGER than the committed bound. The recorder promised a certificate at least that often and none exists for that window. Reported per gap; the open gap (last → now) is reported separately. unspecified the recorder publishes no bound (null). It has promised nothing, so its gaps are `liveness_unspecified` — never "nothing happened". No poll period is published or assumed (a recorder-chosen poll period would be a self-attested number deciding pass/fail). pre_commitment a window that closed at or before `liveness.committed_at` — when the promise came into force. A bound says nothing about silence that predates it, so these are reported, not graded; a window straddling committed_at is graded on its part after it. The time in force is CAPPED at the first heartbeat (a heartbeat exists only because a bound did), so a recorder cannot slide committed_at forward past its own evidence to launder the gaps in between. A missing or unreadable committed_at grades the whole history (fail closed); one in the future binds nothing yet (UNSPECIFIED). Times: the per-checkpoint `created_at` is the SERVER's timestamp. Where a checkpoint carries an anchor with an external timestamp (RFC 3161 TSA `external_ts`), that independent time is used for the gap and the row says `tsa`; otherwise `created_at` is used and the row says `server`. A bound is only as good as the clock it is measured against, so the readout names the clock on every row. Fail closed: any heartbeat contradiction → exit 1. A liveness_gap is a FINDING about the recorder's promise (exit 3), not a tamper. unspecified → exit 0 with the unspecified rows printed (the reader has been told there is no promise). Dependency-free (stdlib). Usage: python3 liveness-verify.py [--base URL] [--now ISO] [--json] python3 liveness-verify.py --selftest """ import datetime as dt import json import sys import urllib.request BASE = "https://touchstone.cv" EXIT_OK, EXIT_CONTRADICTION, EXIT_LOAD, EXIT_GAP = 0, 1, 2, 3 def _iso(s): s = str(s) if s.endswith("Z"): s = s[:-1] + "+00:00" d = dt.datetime.fromisoformat(s) return d if d.tzinfo else d.replace(tzinfo=dt.timezone.utc) HEADERS = {"Accept": "application/json", "User-Agent": "liveness-verify/1"} # the stock urllib agent is refused (403) by the production host's web application firewall def fetch_json(url, timeout=20): req = urllib.request.Request(url, headers=HEADERS) with urllib.request.urlopen(req, timeout=timeout) as r: return json.load(r) def load(src, base=BASE): if src.startswith("http://") or src.startswith("https://"): return fetch_json(src) if src.startswith("rec_"): return fetch_json(f"{base}/.well-known/touchstone/checkpoints/{src}") with open(src, encoding="utf-8") as fh: return json.load(fh) def chain_order(cps): """The server serves oldest-first by (seq_start, id); re-sort defensively on the same key.""" return sorted(cps, key=lambda c: (int(c.get("seq_start", 0)), int(c.get("id", 0)))) def anchor_time(cp): """(datetime, clock) — a TSA anchor's external timestamp when present, else the server's created_at.""" for a in cp.get("anchors") or []: if isinstance(a, dict) and a.get("method") == "tsa" and a.get("external_ts"): try: return _iso(a["external_ts"]), "tsa" except ValueError: pass return _iso(cp["created_at"]), "server" def evaluate(feed, now=None): """Grade a checkpoint feed for liveness. Returns a dict; see RESULT semantics in the module docstring.""" now = now or dt.datetime.now(dt.timezone.utc) liveness = feed.get("liveness") if isinstance(feed.get("liveness"), dict) else {} bound = liveness.get("max_anchor_interval_seconds") bound = bound if isinstance(bound, int) and bound > 0 else None committed_at = None if bound is not None and isinstance(liveness.get("committed_at"), str): try: committed_at = _iso(liveness["committed_at"]) except ValueError: committed_at = None # an unreadable time is no time: the whole history is graded cps = chain_order([c for c in (feed.get("checkpoints") or []) if isinstance(c, dict)]) # In force from committed_at, capped at the first heartbeat: a heartbeat exists only because a bound did. in_force = committed_at for cp in cps: if int(cp.get("seq_end", 0)) < int(cp.get("seq_start", 0)): hb_t, _ = anchor_time(cp) if in_force is not None and hb_t < in_force: in_force = hb_t break def classify(start, end): """A window start→end against the promise in force: (class, graded_seconds).""" gap = int((end - start).total_seconds()) if bound is None: return "unspecified", None if in_force is not None: if end <= in_force: return "pre_commitment", None graded = min(gap, int((end - in_force).total_seconds())) else: graded = gap return ("liveness_gap" if graded > bound else "ok"), graded rows, contradictions = [], 0 prev = None for cp in cps: heartbeat = int(cp.get("seq_end", 0)) < int(cp.get("seq_start", 0)) ok = True note = "" if heartbeat: if prev is None: ok, note = False, "heartbeat with no previous checkpoint to repeat" elif cp.get("merkle_root") != prev.get("merkle_root") or cp.get("head_hash") != prev.get("head_hash") \ or cp.get("prev_checkpoint_hash") != prev.get("head_hash"): ok, note = False, "heartbeat CONTRADICTION: root/head/prev do not repeat the previous checkpoint" if not ok: contradictions += 1 t, clock = anchor_time(cp) gap = None cls = None graded = None if prev is not None: pt, _ = anchor_time(prev) gap = int((t - pt).total_seconds()) cls, graded = classify(pt, t) sb = cp.get("server_beacon") beacon = sb.get("round") if isinstance(sb, dict) and isinstance(sb.get("round"), int) else None rows.append({"id": cp.get("id"), "seq_start": cp.get("seq_start"), "seq_end": cp.get("seq_end"), "heartbeat": heartbeat, "ok": ok, "note": note, "at": t.isoformat(), "clock": clock, "gap_seconds": gap, "graded_seconds": graded, "class": cls, "beacon_round": beacon}) prev = cp open_gap = None open_cls = None open_graded = None if prev is not None: pt, _ = anchor_time(prev) open_gap = int((now - pt).total_seconds()) open_cls, open_graded = classify(pt, now) gaps = [r for r in rows if r["class"] == "liveness_gap"] + ([{"open": True}] if open_cls == "liveness_gap" else []) pre = sum(1 for r in rows if r["class"] == "pre_commitment") + (1 if open_cls == "pre_commitment" else 0) graded_any = any(r["graded_seconds"] is not None for r in rows) or open_graded is not None result = ("FAIL" if contradictions else "LIVENESS_GAP" if gaps else "UNSPECIFIED" if rows and not graded_any else "OK") return {"recorder": feed.get("recorder"), "bound_seconds": bound, "checkpoints": len(cps), "heartbeats": sum(1 for r in rows if r["heartbeat"]), "contradictions": contradictions, "committed_at": committed_at.isoformat() if committed_at else None, "in_force_from": in_force.isoformat() if in_force else None, "rows": rows, "open_gap_seconds": open_gap, "open_graded_seconds": open_graded, "open_class": open_cls, "liveness_gaps": len(gaps), "pre_commitment_windows": pre, "result": result, "now": now.isoformat()} def _fmt(seconds): if seconds is None: return "-" h, rem = divmod(int(seconds), 3600) return f"{h}h{rem // 60:02d}m" if h else f"{rem // 60}m{rem % 60:02d}s" def report(res): b = res["bound_seconds"] hdr = f"recorder {res['recorder']} bound {('none (unspecified)' if b is None else _fmt(b))} checkpoints {res['checkpoints']} heartbeats {res['heartbeats']}" if res.get("in_force_from"): hdr += f" in force since {res['in_force_from'][:19]}Z" if res.get("committed_at") != res.get("in_force_from"): hdr += f" (published {str(res.get('committed_at'))[:19]}Z, capped at the first heartbeat)" print(hdr) for r in res["rows"]: mark = "✗" if not r["ok"] else ("♥" if r["heartbeat"] else "·") graded = r.get("graded_seconds") gap = "" if r["gap_seconds"] is None else f" gap {_fmt(r['gap_seconds'])} [{r['class']}]" + (f" graded {_fmt(graded)}" if graded is not None and graded != r["gap_seconds"] else "") rng = f"{r['seq_start']}–{r['seq_end']}" if not r["heartbeat"] else "empty" beacon = f" round {r['beacon_round']}" if r.get("beacon_round") is not None else "" print(f" {mark} #{r['id']} {rng:>9} {r['at'][:19]}Z ({r['clock']}){beacon}{gap}{(' ' + r['note']) if r['note'] else ''}") if res["open_gap_seconds"] is not None: og = res.get("open_graded_seconds") print(f" … open gap since last checkpoint {_fmt(res['open_gap_seconds'])} [{res['open_class']}]" + (f" graded {_fmt(og)}" if og is not None and og != res["open_gap_seconds"] else "")) pre = res.get("pre_commitment_windows") or 0 pre_note = f" ({pre} window(s) predate the promise and are not graded)" if pre else "" print({ "FAIL": "RESULT: FAIL ✗ — a heartbeat changed the root it claimed to repeat; the feed is contradictory", "LIVENESS_GAP": f"RESULT: LIVENESS_GAP — {res['liveness_gaps']} window(s) longer than the committed bound carry no certificate; the recorder broke its own promise (not a tamper)" + pre_note, "UNSPECIFIED": "RESULT: UNSPECIFIED — this recorder publishes no bound in force; its gaps mean nothing either way", "OK": "RESULT: OK ✓ — every window is within the committed bound and every heartbeat repeats its root" + pre_note, }[res["result"]]) return {"FAIL": EXIT_CONTRADICTION, "LIVENESS_GAP": EXIT_GAP, "UNSPECIFIED": EXIT_OK, "OK": EXIT_OK}[res["result"]] # ───────────────────────── selftest: planted feeds, every rule exercised ───────────────────────── def _cp(i, s, e, root, head, prev, at, tsa=None): cp = {"id": i, "seq_start": s, "seq_end": e, "merkle_root": root, "head_hash": head, "prev_checkpoint_hash": prev, "created_at": at, "anchors": []} if tsa: cp["anchors"].append({"method": "tsa", "external_ts": tsa, "status": "confirmed"}) return cp T0 = dt.datetime(2026, 10, 16, 0, 0, tzinfo=dt.timezone.utc) _t = lambda s: (T0 + dt.timedelta(seconds=s)).isoformat() _R1, _H1, _R2, _H2 = "a" * 64, "b" * 64, "c" * 64, "d" * 64 def selftest_cases(): """The planted feeds behind --selftest, as (name, feed, now) — shared with verifier/liveness_parity.py so the JS twin is graded on exactly the same inputs.""" t = _t; R1, H1, R2, H2 = _R1, _H1, _R2, _H2 honest = {"recorder": "rec_test", "liveness": {"max_anchor_interval_seconds": 3600}, "checkpoints": [ _cp(1, 0, 4, R1, H1, None, t(0)), _cp(2, 5, 4, R1, H1, H1, t(3000)), _cp(3, 5, 9, R2, H2, H1, t(6000))]} yield "honest", honest, T0 + dt.timedelta(seconds=7000) gap = {"recorder": "rec_test", "liveness": {"max_anchor_interval_seconds": 3600}, "checkpoints": [ _cp(1, 0, 4, R1, H1, None, t(0)), _cp(2, 5, 9, R2, H2, H1, t(3601))]} yield "gap one over", gap, T0 + dt.timedelta(seconds=3700) yield "gap exactly at bound", {**gap, "checkpoints": [gap["checkpoints"][0], _cp(2, 5, 9, R2, H2, H1, t(3600))]}, T0 + dt.timedelta(seconds=3650) bad = {"recorder": "rec_test", "liveness": {"max_anchor_interval_seconds": 3600}, "checkpoints": [ _cp(1, 0, 4, R1, H1, None, t(0)), _cp(2, 5, 4, R2, H1, H1, t(1000))]} yield "heartbeat root changed", bad, T0 + dt.timedelta(seconds=1100) yield "heartbeat prev link wrong", {**bad, "checkpoints": [bad["checkpoints"][0], _cp(2, 5, 4, R1, H1, "e" * 64, t(1000))]}, T0 + dt.timedelta(seconds=10_000) yield "heartbeat first", {"recorder": "r", "liveness": {"max_anchor_interval_seconds": 60}, "checkpoints": [_cp(1, 1, 0, R1, H1, None, t(0))]}, T0 + dt.timedelta(seconds=10_000) yield "no bound", {"recorder": "rec_test", "liveness": {"max_anchor_interval_seconds": None}, "checkpoints": gap["checkpoints"]}, T0 + dt.timedelta(seconds=10_000) yield "no liveness block", {"recorder": "r", "checkpoints": gap["checkpoints"]}, T0 + dt.timedelta(seconds=10_000) yield "open gap over", honest, T0 + dt.timedelta(seconds=6000 + 5000) cpA = _cp(1, 0, 4, R1, H1, None, t(0), tsa=t(100)); cpB = _cp(2, 5, 9, R2, H2, H1, t(3650), tsa=t(3690)) yield "tsa clock", {"recorder": "r", "liveness": {"max_anchor_interval_seconds": 3600}, "checkpoints": [cpA, cpB]}, T0 + dt.timedelta(seconds=3700) yield "shuffled", {**honest, "checkpoints": list(reversed(honest["checkpoints"]))}, T0 + dt.timedelta(seconds=7000) yield "zero bound", {**gap, "liveness": {"max_anchor_interval_seconds": 0}}, T0 + dt.timedelta(seconds=10_000) yield "string bound", {**gap, "liveness": {"max_anchor_interval_seconds": "3600"}}, T0 + dt.timedelta(seconds=10_000) beaconed = {**honest, "checkpoints": [dict(c, server_beacon={"chain": "drand:quicknet", "round": 1000 + i, "randomness": "f" * 64}) for i, c in enumerate(honest["checkpoints"])]} yield "beacon rounds", beaconed, T0 + dt.timedelta(seconds=7000) yield "empty feed", {"recorder": "r", "liveness": {"max_anchor_interval_seconds": 3600}, "checkpoints": []}, T0 # committed_at: the promise binds from when it was made yield "pre-commitment gap", {**gap, "liveness": {"max_anchor_interval_seconds": 3600, "committed_at": t(5000)}}, T0 + dt.timedelta(seconds=5100) strad = {"recorder": "r", "liveness": {"max_anchor_interval_seconds": 3600, "committed_at": t(3000)}, "checkpoints": [ _cp(1, 0, 4, R1, H1, None, t(0)), _cp(2, 5, 9, R2, H2, H1, t(7000))]} yield "straddling window over", strad, T0 + dt.timedelta(seconds=7100) yield "straddling window within", {**strad, "checkpoints": [strad["checkpoints"][0], _cp(2, 5, 9, R2, H2, H1, t(6000))]}, T0 + dt.timedelta(seconds=6100) yield "commitment in the future", {**gap, "liveness": {"max_anchor_interval_seconds": 3600, "committed_at": t(20000)}}, T0 + dt.timedelta(seconds=10_000) capped = {"recorder": "r", "liveness": {"max_anchor_interval_seconds": 3600, "committed_at": t(6000)}, "checkpoints": [ _cp(1, 0, 4, R1, H1, None, t(0)), _cp(2, 5, 4, R1, H1, H1, t(3000)), _cp(3, 5, 9, R2, H2, H1, t(7000))]} yield "commitment after a heartbeat is capped", capped, T0 + dt.timedelta(seconds=7100) yield "unreadable commitment time", {**gap, "liveness": {"max_anchor_interval_seconds": 3600, "committed_at": "yesterday"}}, T0 + dt.timedelta(seconds=3700) def _selftest(): t = _t now = T0 + dt.timedelta(seconds=10_000) R1, H1, R2, H2 = _R1, _H1, _R2, _H2 fails = [] def check(name, cond): if not cond: fails.append(name) print(("ok " if cond else "FAIL ") + name) # 1. honest: real cp, heartbeat at 3000s (same root), real cp at 6000s; bound 3600 → OK feed = {"recorder": "rec_test", "liveness": {"max_anchor_interval_seconds": 3600}, "checkpoints": [ _cp(1, 0, 4, R1, H1, None, t(0)), _cp(2, 5, 4, R1, H1, H1, t(3000)), _cp(3, 5, 9, R2, H2, H1, t(6000))]} r = evaluate(feed, now=T0 + dt.timedelta(seconds=7000)) check("honest feed within bound is OK", r["result"] == "OK" and r["heartbeats"] == 1 and r["contradictions"] == 0) # 2. a gap of 3601s between checkpoints → LIVENESS_GAP (strict: > bound) feed2 = {"recorder": "rec_test", "liveness": {"max_anchor_interval_seconds": 3600}, "checkpoints": [ _cp(1, 0, 4, R1, H1, None, t(0)), _cp(2, 5, 9, R2, H2, H1, t(3601))]} r2 = evaluate(feed2, now=T0 + dt.timedelta(seconds=3700)) check("gap one second over the bound is liveness_gap", r2["result"] == "LIVENESS_GAP" and r2["rows"][1]["class"] == "liveness_gap") check("gap exactly at the bound is ok", evaluate({**feed2, "checkpoints": [feed2["checkpoints"][0], _cp(2, 5, 9, R2, H2, H1, t(3600))]}, now=T0 + dt.timedelta(seconds=3650))["rows"][1]["class"] == "ok") # 3. heartbeat that changes the root → FAIL (contradiction), regardless of timing feed3 = {"recorder": "rec_test", "liveness": {"max_anchor_interval_seconds": 3600}, "checkpoints": [ _cp(1, 0, 4, R1, H1, None, t(0)), _cp(2, 5, 4, R2, H1, H1, t(1000))]} r3 = evaluate(feed3, now=T0 + dt.timedelta(seconds=1100)) check("heartbeat with a different root is a contradiction", r3["result"] == "FAIL" and r3["contradictions"] == 1) check("heartbeat whose prev link does not match is a contradiction", evaluate({**feed3, "checkpoints": [feed3["checkpoints"][0], _cp(2, 5, 4, R1, H1, "e" * 64, t(1000))]}, now=now)["contradictions"] == 1) check("a heartbeat as the very first checkpoint is a contradiction", evaluate({"recorder": "r", "liveness": {"max_anchor_interval_seconds": 60}, "checkpoints": [_cp(1, 1, 0, R1, H1, None, t(0))]}, now=now)["contradictions"] == 1) # 4. no bound → UNSPECIFIED, exit 0, every gap class unspecified (never "ok") feed4 = {"recorder": "rec_test", "liveness": {"max_anchor_interval_seconds": None}, "checkpoints": feed2["checkpoints"]} r4 = evaluate(feed4, now=now) check("no published bound is UNSPECIFIED, not ok", r4["result"] == "UNSPECIFIED" and all(x["class"] in (None, "unspecified") for x in r4["rows"]) and r4["open_class"] == "unspecified") check("a feed with no liveness block at all is UNSPECIFIED", evaluate({"recorder": "r", "checkpoints": feed2["checkpoints"]}, now=now)["result"] == "UNSPECIFIED") # 5. open gap: last checkpoint 5000s ago with bound 3600 → LIVENESS_GAP on the open window only r5 = evaluate(feed, now=T0 + dt.timedelta(seconds=6000 + 5000)) check("open window longer than the bound is a liveness_gap", r5["result"] == "LIVENESS_GAP" and r5["open_class"] == "liveness_gap" and all(x["class"] != "liveness_gap" for x in r5["rows"])) # 6. clock: a TSA external_ts is preferred over created_at and named on the row cpA = _cp(1, 0, 4, R1, H1, None, t(0), tsa=t(100)); cpB = _cp(2, 5, 9, R2, H2, H1, t(3650), tsa=t(3690)) r6 = evaluate({"recorder": "r", "liveness": {"max_anchor_interval_seconds": 3600}, "checkpoints": [cpA, cpB]}, now=T0 + dt.timedelta(seconds=3700)) check("TSA time is used and named when present", r6["rows"][1]["clock"] == "tsa" and r6["rows"][1]["gap_seconds"] == 3590 and r6["rows"][1]["class"] == "ok") # 7. order: the reader re-sorts by (seq_start, id) so a shuffled feed grades the same r7 = evaluate({**feed, "checkpoints": list(reversed(feed["checkpoints"]))}, now=T0 + dt.timedelta(seconds=7000)) check("shuffled feed grades identically", r7["result"] == r["result"] and [x["id"] for x in r7["rows"]] == [1, 2, 3]) # 8. a non-positive or non-integer bound is treated as unspecified (never as a tiny promise) check("a non-positive bound is unspecified", evaluate({**feed2, "liveness": {"max_anchor_interval_seconds": 0}}, now=now)["result"] == "UNSPECIFIED") check("a string bound is unspecified", evaluate({**feed2, "liveness": {"max_anchor_interval_seconds": "3600"}}, now=now)["result"] == "UNSPECIFIED") # 9. beacon round is carried per row when present (informational), absent → None cases = {n: (f, w) for n, f, w in selftest_cases()} rb = evaluate(*cases["beacon rounds"]) check("beacon rounds ride on the rows", [x["beacon_round"] for x in rb["rows"]] == [1000, 1001, 1002] and all(x["beacon_round"] is None for x in r["rows"])) check("an empty feed with a bound is OK with no rows", evaluate(*cases["empty feed"])["result"] == "OK") # 10. committed_at: windows that closed before the promise are pre_commitment, never liveness_gap r10 = evaluate(*cases["pre-commitment gap"]) check("a gap that closed before the promise is pre_commitment and the feed is OK", r10["result"] == "OK" and r10["rows"][1]["class"] == "pre_commitment" and r10["pre_commitment_windows"] == 1 and r10["open_class"] == "ok") r11 = evaluate(*cases["straddling window over"]) check("a window straddling the promise is graded on its part after it (over)", r11["result"] == "LIVENESS_GAP" and r11["rows"][1]["gap_seconds"] == 7000 and r11["rows"][1]["graded_seconds"] == 4000) r12 = evaluate(*cases["straddling window within"]) check("a window straddling the promise is graded on its part after it (within)", r12["result"] == "OK" and r12["rows"][1]["graded_seconds"] == 3000 and r12["rows"][1]["class"] == "ok") r13 = evaluate(*cases["commitment in the future"]) check("a promise that starts in the future binds nothing yet: UNSPECIFIED", r13["result"] == "UNSPECIFIED" and r13["open_class"] == "pre_commitment") r14 = evaluate(*cases["commitment after a heartbeat is capped"]) check("committed_at later than a heartbeat is capped at that heartbeat", r14["in_force_from"] == t(3000) and r14["result"] == "LIVENESS_GAP" and r14["rows"][2]["graded_seconds"] == 4000) r15 = evaluate(*cases["unreadable commitment time"]) check("an unreadable committed_at grades the whole history", r15["result"] == "LIVENESS_GAP" and r15["committed_at"] is None and r15["in_force_from"] is None) check("without committed_at every window is graded, as before", r2["in_force_from"] is None and r2["rows"][1]["graded_seconds"] == 3601) print(("SELFTEST OK — %d checks" % 21) if not fails else ("SELFTEST FAIL: " + ", ".join(fails))) return 0 if not fails else 1 def main(argv): if "--selftest" in argv: return _selftest() args = [a for a in argv[1:] if not a.startswith("--")] base = BASE now = None if "--base" in argv: base = argv[argv.index("--base") + 1].rstrip("/") args = [a for a in args if a != base] if "--now" in argv: now = _iso(argv[argv.index("--now") + 1]) args = [a for a in args if a != argv[argv.index("--now") + 1]] if not args: print(__doc__) return EXIT_LOAD try: feed = load(args[0], base) except Exception as e: # noqa: BLE001 print(f"could not load feed: {e}", file=sys.stderr) return EXIT_LOAD if not isinstance(feed, dict) or "checkpoints" not in feed: print("not a touchstone checkpoint feed (no checkpoints key)", file=sys.stderr) return EXIT_LOAD res = evaluate(feed, now=now) if "--json" in argv: print(json.dumps(res, indent=2)) return {"FAIL": EXIT_CONTRADICTION, "LIVENESS_GAP": EXIT_GAP}.get(res["result"], EXIT_OK) return report(res) if __name__ == "__main__": sys.exit(main(sys.argv))