#!/usr/bin/env python3 """ Touchstone cross-recorder reconciliation — pure Python 3 stdlib, no dependencies. A tamper-evident log proves the integrity of what you *logged*, never the completeness of what you *did*: an agent can faithfully record a subset and silently omit a shared event, and the hash chain still verifies. The only thing that bounds that gap for a two-party interaction is the *other* party's record. This tool reconciles two recorders' disclosed logs and surfaces where they disagree about the events they shared. A shared event is one party's entry naming the other as counterparty. Identified across the two logs by `payload_hash` (the hash of the shared interaction's content), it should appear from BOTH sides: BILATERAL — A logged (counterparty=B, payload_hash=H) and B logged (counterparty=A, H). Both sides independently recorded the same interaction. Strongest state. CO-SIGNED — A's entry carries B's counterparty signature over the same canonical bytes. B cryptographically acknowledged the event; B cannot later deny it happened, whether or not B mirrored it in its own log. ONE-SIDED — A logged a shared event B's disclosed log has no mirror for. This is a CANDIDATE, not a verdict: B may simply not record its side, or not have disclosed that entry. It is NOT proof of suppression. What it proves: the two logs agree (or don't) on their shared events, and which events each side cryptographically acknowledged. What it does NOT prove on its own: that a one-sided event was *suppressed* — distinguishing "B omitted it" from "B never recorded its side" needs B to hold a co-signature receipt for the event and present it against A's disclosed log. That receipt mechanism is the next primitive; this tool is its detection half. Verify each bundle's integrity first (verify.php / touchstone-verify / the MCP tool) — this tool cross-references already-verified entries; it does not re-check signatures except the counterparty co-signatures it reports as CO-SIGNED. Usage: python3 reconcile.py python3 reconcile.py --selftest Exit: 0 = fully reconciled (no one-sided shared events) · 1 = asymmetry found · 2 = bad input """ import sys import json def _index(bundle): """(subject_sub, {payload_hash: [entries naming the *other* side]}, all_entries).""" rec = bundle.get("recorder") or {} subject = rec.get("subject_sub") entries = bundle.get("entries") or [] return subject, entries def reconcile(bundle_a, bundle_b): """→ (rows, summary). rows: per shared event, its state from A's and B's side.""" sub_a, entries_a = _index(bundle_a) sub_b, entries_b = _index(bundle_b) if not sub_a or not sub_b: raise ValueError("each bundle must carry recorder.subject_sub") pub_b = (bundle_b.get("recorder") or {}).get("signing_pubkey") pub_a = (bundle_a.get("recorder") or {}).get("signing_pubkey") # Events each side recorded as shared with the other, keyed by payload_hash. def shared(entries, me, other): out = {} for e in entries: if e.get("counterparty_sub") == other and e.get("actor_sub") in (me, None): out.setdefault(e.get("payload_hash"), []).append(e) return out a_shared = shared(entries_a, sub_a, sub_b) # A's entries naming B b_shared = shared(entries_b, sub_b, sub_a) # B's entries naming A def cosigned_by(e, expected_pub): sig = e.get("counterparty_sig") pk = e.get("counterparty_pubkey") # CO-SIGNED requires a key that matches the counterparty's own recorder key. return bool(sig) and bool(pk) and (expected_pub is None or pk == expected_pub) rows = [] for h in sorted(set(a_shared) | set(b_shared), key=lambda x: (x is None, x)): in_a = h in a_shared in_b = h in b_shared a_cos = any(cosigned_by(e, pub_b) for e in a_shared.get(h, [])) b_cos = any(cosigned_by(e, pub_a) for e in b_shared.get(h, [])) if in_a and in_b: state = "BILATERAL" elif a_cos or b_cos: state = "CO-SIGNED" # one-sided in logs, but the other party signed it else: state = "ONE-SIDED" rows.append({ "payload_hash": h, "state": state, "in_a": in_a, "in_b": in_b, "a_cosigned_by_b": a_cos, "b_cosigned_by_a": b_cos, }) one_sided = [r for r in rows if r["state"] == "ONE-SIDED"] summary = { "subject_a": sub_a, "subject_b": sub_b, "shared_events": len(rows), "bilateral": sum(1 for r in rows if r["state"] == "BILATERAL"), "cosigned": sum(1 for r in rows if r["state"] == "CO-SIGNED"), "one_sided": len(one_sided), } return rows, summary def _print_report(rows, summary): print(f"Reconciling {summary['subject_a']} ⇄ {summary['subject_b']}") print(f" {summary['shared_events']} shared event(s): " f"{summary['bilateral']} bilateral, {summary['cosigned']} co-signed one-side, " f"{summary['one_sided']} one-sided\n") for r in rows: h = (r["payload_hash"] or "—")[:16] if r["state"] == "BILATERAL": print(f" [✓] {h}… BILATERAL — both logs recorded it") elif r["state"] == "CO-SIGNED": who = "A's entry co-signed by B" if r["a_cosigned_by_b"] else "B's entry co-signed by A" print(f" [✓] {h}… CO-SIGNED — one-sided in logs, but {who}; acknowledged, cannot be denied") else: side = "A only (B has no mirror)" if r["in_a"] else "B only (A has no mirror)" print(f" [?] {h}… ONE-SIDED — {side}; candidate for investigation, not proof of omission") if summary["one_sided"]: print(f"\n\033[33m{summary['one_sided']} one-sided shared event(s)\033[0m — each is a recorded " "interaction the other side's disclosed log does not mirror. Could be a side that doesn't " "record, an undisclosed entry, or an omission. To settle it, have the other party present a " "co-signature receipt for the event against the discloser's log.") else: print("\n\033[32mFully reconciled\033[0m — every shared event appears from both sides or is co-signed.") def _selftest(): pub_b = "B" * 44 pub_a = "A" * 44 # H1 bilateral, H2 co-signed-by-B (one-sided in logs), H3 one-sided (no signature) A = {"recorder": {"subject_sub": "agentA", "signing_pubkey": pub_a}, "entries": [ {"actor_sub": "agentA", "counterparty_sub": "agentB", "payload_hash": "H1"}, {"actor_sub": "agentA", "counterparty_sub": "agentB", "payload_hash": "H2", "counterparty_sig": "sig", "counterparty_pubkey": pub_b}, {"actor_sub": "agentA", "counterparty_sub": "agentB", "payload_hash": "H3"}, ]} B = {"recorder": {"subject_sub": "agentB", "signing_pubkey": pub_b}, "entries": [ {"actor_sub": "agentB", "counterparty_sub": "agentA", "payload_hash": "H1"}, ]} rows, summary = reconcile(A, B) by_h = {r["payload_hash"]: r for r in rows} assert by_h["H1"]["state"] == "BILATERAL", by_h["H1"] assert by_h["H2"]["state"] == "CO-SIGNED" and by_h["H2"]["a_cosigned_by_b"], by_h["H2"] assert by_h["H3"]["state"] == "ONE-SIDED", by_h["H3"] assert summary == {"subject_a": "agentA", "subject_b": "agentB", "shared_events": 3, "bilateral": 1, "cosigned": 1, "one_sided": 1}, summary # A forged "co-signature" under the wrong key must NOT count as CO-SIGNED. A2 = json.loads(json.dumps(A)) A2["entries"][1]["counterparty_pubkey"] = "Z" * 44 # not B's key rows2, _ = reconcile(A2, B) assert {r["payload_hash"]: r for r in rows2}["H2"]["state"] == "ONE-SIDED", "wrong-key sig must not be CO-SIGNED" print("reconcile selftest: OK") def main(): if "--selftest" in sys.argv: _selftest() return 0 args = [a for a in sys.argv[1:] if not a.startswith("--")] if len(args) != 2: print("usage: reconcile.py (or --selftest)") return 2 try: a = json.load(open(args[0])) b = json.load(open(args[1])) except Exception as e: print(f"could not read a bundle: {e}") return 2 try: rows, summary = reconcile(a, b) except ValueError as e: print(f"cannot reconcile: {e}") return 2 _print_report(rows, summary) return 1 if summary["one_sided"] else 0 if __name__ == "__main__": sys.exit(main())