-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrun_provenance_defense_study.py
More file actions
125 lines (105 loc) · 4.61 KB
/
Copy pathrun_provenance_defense_study.py
File metadata and controls
125 lines (105 loc) · 4.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/usr/bin/env python3
"""Run the provenance-forced-swap synthesis study.
Scores the same forced-recycle sweep on both the maker side (the strong global
matching attacker reconstructing wallets) and the taker side (exit anonymity),
plus economics (liquidity, throughput, swap fee), to answer whether forced swap
recycling breaks maker clustering and restores taker anonymity at once:
* D0 -- no-defense baseline (q=0), per fee regime;
* D1 -- equal-output-only forced swap (the naive deployment shape), q sweep;
* D2 -- all-due forced swap (faithful due-input rejection), q sweep.
Writes ``provenance_defense_results.json``.
Usage::
python run_provenance_defense_study.py # full default scale
python run_provenance_defense_study.py --quick # tiny smoke run
"""
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
from coinjoin_simulator.provenance_defense_study import DefenseStudyConfig, build_study
OUTPUT_PATH = Path("provenance_defense_results.json")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--quick", action="store_true", help="tiny smoke run")
parser.add_argument("--makers", type=int, default=60)
parser.add_argument("--takers-per-maker", type=int, default=4)
parser.add_argument("--makers-per-cj", type=int, default=5)
parser.add_argument("--seeds", type=int, default=3)
args = parser.parse_args()
if args.quick:
cfg = DefenseStudyConfig(
n_makers=16,
takers_per_maker=4,
makers_per_cj=5,
q_values=(10, 4, 2),
seeds=(101,),
)
else:
seed_pool = (101, 202, 303, 404, 505)
cfg = DefenseStudyConfig(
n_makers=args.makers,
takers_per_maker=args.takers_per_maker,
makers_per_cj=args.makers_per_cj,
seeds=tuple(seed_pool[: args.seeds]),
)
start = time.time()
print(
f"Running provenance defense study: {cfg.n_makers} makers, "
f"{cfg.takers_per_maker * cfg.n_makers} takers, "
f"{len(cfg.seeds)} seeds, q in {cfg.q_values} ..."
)
study = build_study(cfg)
payload = study.payload
payload["generated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
OUTPUT_PATH.write_text(json.dumps(payload, indent=2) + "\n")
elapsed = time.time() - start
print(f"\nWrote {OUTPUT_PATH} in {elapsed:.1f}s\n")
_print_summary(payload)
def _print_summary(payload: dict) -> None:
def table(title: str, rows: list[dict]) -> None:
print(f"\n{title}")
print(
f" {'scenario':<33} {'m_recall':>8} {'m_full':>7} {'exit_dn':>8} "
f"{'liq':>6} {'cj':>6} {'fee/cj':>7}"
)
for r in rows:
print(
f" {r['label'][:33]:<33} "
f"{r['maker_recall_mean']:>8.3f} "
f"{r['maker_full_recovery_mean']:>7.3f} "
f"{r['exit_deanon_mean']:>8.3f} "
f"{r['liquidity_retained_mean']:>6.2f} "
f"{r['completed_coinjoins_mean']:>6.0f} "
f"{r['swap_fee_per_cj_mean']:>7.0f}"
)
table("D0 - no-defense baseline", payload["d0_baseline"])
table("D1 - equal-output-only forced swap", payload["d1_equal_only_forced_swap"])
table("D2 - all-due forced swap", payload["d2_all_due_forced_swap"])
h = payload["headline"]
print("\nHeadline:")
print(
f" baseline (distinct fees): maker recall {h['baseline_distinct_maker_recall']:.2f}, "
f"full recovery {h['baseline_distinct_maker_full_recovery']:.2f}, "
f"taker exit deanon {h['baseline_distinct_exit_deanon']:.2f}"
)
print(
f" equal-only q=2 (distinct): maker recall "
f"{h['equal_only_distinct_q2_maker_recall']:.2f} (makers NOT broken), "
f"taker exit deanon {h['equal_only_distinct_q2_exit_deanon']:.2f} (taker fixed)"
)
print(
f" all-due distinct: q=4 recall {h['all_due_distinct_q4_maker_recall']:.2f} / "
f"full {h['all_due_distinct_q4_maker_full_recovery']:.2f}, "
f"q=2 recall {h['all_due_distinct_q2_maker_recall']:.2f} / "
f"full {h['all_due_distinct_q2_maker_full_recovery']:.2f} (the recall/full gap)"
)
print(
f" all-due q=2 economics: liquidity {h['all_due_distinct_q2_liquidity_retained']:.2f}, "
f"cj {h['all_due_distinct_q2_completed_coinjoins']:.0f} "
f"(baseline {h['baseline_distinct_completed_coinjoins']:.0f}), "
f"fee {h['all_due_distinct_q2_swap_fee_per_cj']:.0f} sat/cj"
)
print(f"\n {h['verdict']}")
if __name__ == "__main__":
main()