#!/usr/bin/env python3 """What each attack costs in rented hash and what it earns: the table behind docs/analysis/51-percent.md. Inputs (every one labelled): * price: USD 11.7 per GH/s-hour, MEASURED 6 Oct 2026 on RunPod community pods (docs/bench-log.md, "Rental cost of hash, 6 October 2026": 1,748 MH/s for USD 20.44 an hour); approximate above 2 GH/s because the market supplied no more. * subsidy: 31.688 IGN per DAA second at full ramp before the first halving (spec 02 2.5, BASE_SUBSIDY_PER_SECOND_SOMPI), 80% to the producer, 20% to the proving pool (the attacker earns the pool share only if it proves: not counted). * IGN price inputs USD 0.005, 0.02, 0.10: the three assumptions of docs/analysis/security-budget.md, NOT predictions. * durations: from the finality arithmetic (spec 03 3.1: 1/3 on day 10/A, 2/3 on day 20/A for an attacker producing share A of blocks), the lock latency (spec 03 3.11.3, about 90 to 120 s), the finality depth (spec 02 2.1, 12 h), the merge depth (3,600 s) and the P2 signalling window (one day). The DAG simulator of this directory gives the hash share needed to win the lock-latency race (ghostdag_results_1bps.md section 2). An attacker at share A of the TOTAL hash rents A/(1 - A) times the honest network N. Run: python3 sim/horizon/consensus-security/cost_model.py [--out file.md] """ import argparse import sys PRICE = 11.7 # USD per GH/s-hour, measured SUBSIDY_IGN_PER_S = 31.688 # spec 02 2.5 PRODUCER_SHARE = 0.8 IGN_PRICES = (0.005, 0.02, 0.10) # security-budget.md inputs NETWORKS = (1, 10, 100, 1000) # GH/s def ign_per_hour(A): return PRODUCER_SHARE * A * SUBSIDY_IGN_PER_S * 3600.0 def md(headers, rows): out = ["| " + " | ".join(headers) + " |", "|" + "---|" * len(headers)] for r in rows: out.append("| " + " | ".join(str(x) for x in r) + " |") return "\n".join(out) def usd(x): if x >= 1e6: return "USD %.1fM" % (x / 1e6) if x >= 1e3: return "USD %.0fk" % (x / 1e3) return "USD %.0f" % x ATTACKS = [ # (name, share A, hours, what it buys, bound) ("Reorder the last ~20 s (the k-block head start, any share)", 0.20, 30 / 3600.0, "an earlier-ordered conflicting tx inside the last k honest blocks; nothing a lock covers", "k = 18 blocks; the lock covers it"), ("Win the lock-latency race: reorg up to the unlocked checkpoint (90 s)", 0.51, 90 / 3600.0, "a double spend of a deposit credited before the lock (no exchange should)", "the lock, 90 to 120 s; DAG sim: 45% wins half, 34% never at 90 s"), ("PoW double spend at the 12-h finality depth during a PAUSE or the first 30 days", 0.51, 12.0, "a deposit credited at the proof-of-work depth the exchange guidance names", "finality depth 43,200 DAA; only while nothing locks"), ("Orphan an hour of honest blocks (beyond merge depth) during a pause", 0.51, 1.5, "honest blocks of the hour unmergeable, their subsidy lost; the 229-block shape of 6 Oct", "merge depth 3,600 s; refused by F1 once a lock exists inside the hour"), ("The veto: reach 1/3 of 30-day weight at 51% of blocks", 0.51, 20 * 24.0, "pause finality at will, for ever, at no further cost (silent weight keeps earning)", "20 days in public; 51% never reaches 2/3 while honest miners stay"), ("The veto at 67% of blocks", 0.67, 15 * 24.0, "the same, five days sooner", "day 15"), ("The veto at 90% of blocks", 0.90, 11.1 * 24.0, "the same", "day 11.1"), ("Lock alone: 2/3 of weight at 67% of blocks", 0.67, 30 * 24.0, "certify any chain forward of the last honest lock; never undo a certificate an honest node holds", "day 30; 3.11.4"), ("Lock alone at 90% of blocks", 0.90, 22.2 * 24.0, "the same", "day 22.2"), ("Long-range: a private DAG heavier than the public one over the window (cold-start F5)", 0.51, 30 * 24.0, "a cold node with no trusted certificate follows the private DAG", "F5: a configured certificate; the client-shipped checkpoint (proposal)"), ("Signalling holdout: 6% of blocks every day until the floor", 0.06, 24.0, "delay of a class change until N6", "the floor"), ("Forced flip: 95% of one day's blue blocks with a patched byte", 0.95, 24.0, "an activation while part of the fleet lacks the object (a v5 shape, not v4)", "the one-day window; 7 consecutive days proposed"), ] def main(argv=None): ap = argparse.ArgumentParser() ap.add_argument("--out", default="") args = ap.parse_args(argv) out = ["# Attack cost in rented hash against what it earns", ""] out.append("Generated by `sim/horizon/consensus-security/cost_model.py`. Price USD %.1f per GH/s-hour (measured 6 Oct 2026, bench-log); subsidy %.3f IGN/s, producer share %.0f%%; IGN price inputs USD %s (assumptions, security-budget.md). " "An attacker at share A rents A/(1-A) x N. Earnings are the attacker's own block subsidy over the attack (it mines in public while it accumulates weight)." % (PRICE, SUBSIDY_IGN_PER_S, 100 * PRODUCER_SHARE, ", ".join("%g" % p for p in IGN_PRICES))) out.append("") out.append("## 1. Cost by network size") out.append("") rows = [] for name, A, hours, buys, bound in ATTACKS: rented = A / (1 - A) costs = [rented * N * hours * PRICE for N in NETWORKS] rows.append([name, "%.0f%%" % (100 * A), "%.1f h" % hours if hours < 48 else "%.1f d" % (hours / 24), "%.2f N" % rented] + [usd(c) for c in costs] + [bound]) out.append(md(["attack", "share A", "duration", "rented hash"] + ["cost at %d GH/s" % N for N in NETWORKS] + ["what bounds it"], rows)) out.append("") out.append("## 2. What the attacker earns meanwhile (its own blocks, 80% producer share), and the net at each price input") out.append("") rows = [] for name, A, hours, buys, bound in ATTACKS: earn_ign = ign_per_hour(A) * hours cells = [] for P in IGN_PRICES: e = earn_ign * P cells.append(usd(e)) rows.append([name, "%.0f M IGN" % (earn_ign / 1e6) if earn_ign >= 1e6 else "%.0f k IGN" % (earn_ign / 1e3)] + cells + [buys]) out.append(md(["attack", "subsidy earned"] + ["at USD %g" % P for P in IGN_PRICES] + ["what it buys"], rows)) out.append("") out.append("## 3. The equilibrium network: where rented hash earns its rent") out.append("") out.append("If every GH/s is rented at the measured price, hash joins until the hourly producer subsidy equals the hourly rent: N_eq = 0.8 x 31.688 x 3600 x P / 11.7 GH/s. Attack costs at N_eq are multiples of the chain's own hourly income, so the table below is price-independent in shape.") out.append("") rows = [] for P in IGN_PRICES: hourly = ign_per_hour(1.0) * P neq = hourly / PRICE veto = 0.51 / 0.49 * neq * 480 * PRICE veto_earn = ign_per_hour(0.51) * 480 * P alone = 2.0 * neq * 720 * PRICE alone_earn = ign_per_hour(0.67) * 720 * P ds = 0.51 / 0.49 * neq * 12 * PRICE rows.append(["USD %g" % P, usd(hourly), "%.0f GH/s" % neq, usd(veto), usd(veto_earn), usd(veto - veto_earn), usd(alone), usd(alone_earn), usd(alone - alone_earn), usd(ds)]) out.append(md(["IGN price", "hourly producer subsidy", "N_eq", "veto: 51% for 20 d, rent", "earned", "net", "lock alone: 67% for 30 d, rent", "earned", "net", "12-h double spend in a pause, rent"], rows)) out.append("") out.append("Reading: at the equilibrium the veto's net cost is about 48% of 20 days of the chain's subsidy (the attacker earns 51% of it back), and locking alone nets about 33% of 30 days of subsidy. Both are public for weeks. The 12-hour double spend during a pause costs about 12.5 hours of the chain's subsidy and is the cheapest line in the table: it is why the pause is the residual risk of docs/analysis/51-percent.md section 4.") text = "\n".join(out) if args.out: with open(args.out, "w") as fh: fh.write(text + "\n") print(text) return 0 if __name__ == "__main__": sys.exit(main())