igneum/sim/horizon/consensus-security/cost_model.py
igneum-labs 7a69daa5ef Horizon: lane 1 (consensus-security) lands, with the 51 percent paper
docs/analysis/51-percent.md: what a 51 percent attacker can and cannot do on Igneum, with
numbers (the selected-chain race over a 90-s hold, the lock as the reorder bound, the veto
at 1/3 of weight and its rental cost, the departure case and the LEAVE item, the p2p surface).
docs/analysis/horizon/consensus-security.md: the attack catalogue across GHOSTDAG ordering,
the difficulty rule, the finality weight, miner signalling, proof records, the exec layer and
p2p, each with the bound and the rental cost at the measured USD 11.7 per GH/s-hour; the
pruned-node unwrap class with its sibling list in the sync and IBD flows; fourteen ranked
defences, three of them not recommended with the reason.

Models and results: sim/horizon/consensus-security/ (ghostdag_sim.py, finality_horizon.py,
cost_model.py, signalling.py, result files).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 20:07:43 +00:00

120 lines
7.9 KiB
Python

#!/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())