igneum/sim/horizon/consensus-security/signalling.py
igneum-labs 4e2a4cd647 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

110 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""The miner-signalling game (P2, docs/plans/counter-asic-3-node.md section 6): what a holdout, a renter and a
signal-then-defect miner can do against the 95 percent threshold over a one-day window with a floor height.
Rule as proposed (6 October 2026): epoch e is the new class when (a) its start is at or past the floor N6, or (b) the
previous epoch was, or (c) the window of W = 86,400 DAA below the epoch's seed block is full and at least 9,500 bps of
its blue blocks carry the new object byte. Weight = blue blocks (the finality walk). The byte is set by the node that
builds the template, so a pool's hashers cannot change it and a renter needs only a patched node to set it.
This file is arithmetic, not a stochastic simulation: with 86,400 blocks in the window the binomial noise on the share is
sqrt(p (1 - p) / 86,400) = 0.07 points at p = 0.95, so "95 percent" is a hard line, not a coin flip. Cost basis: the
measured rental price USD 11.7 per GH/s-hour (docs/bench-log.md, "Rental cost of hash, 6 October 2026": 1,748 MH/s for
USD 20.44 an hour on RunPod community pods, approximate at scale because the market could not supply a TH/s).
Run: python3 sim/horizon/consensus-security/signalling.py [--out file.md]
"""
import argparse
import math
import sys
PRICE_PER_GHS_HOUR = 11.7 # USD, measured 6 Oct 2026 (bench-log line "Rental cost of hash")
W_BLOCKS = 86_400
THRESHOLD = 0.95
NETWORKS_GHS = [1, 10, 100, 1000]
def binom_flip_prob(p, n=W_BLOCKS, thr=THRESHOLD):
"""P(share >= thr) for a true signalling fraction p over n blocks, normal approximation."""
if p <= 0 or p >= 1:
return 1.0 if p >= thr else 0.0
mu, sd = p, math.sqrt(p * (1 - p) / n)
z = (thr - mu) / sd
return 0.5 * math.erfc(z / math.sqrt(2))
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 main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="")
args = ap.parse_args(argv)
out = ["# Miner signalling: the 95 percent game, one-day window, floor height", ""]
out.append("Generated by `sim/horizon/consensus-security/signalling.py`. Arithmetic on the P2 rule; price USD %.1f per GH/s-hour (measured, bench-log 6 Oct 2026)." % PRICE_PER_GHS_HOUR)
out.append("")
out.append("## 1. The threshold is a hard line: binomial noise over 86,400 blocks")
out.append("")
rows = []
for p in (0.93, 0.94, 0.945, 0.949, 0.95, 0.951, 0.955, 0.96):
rows.append(["%.1f%%" % (100 * p), "%.2f points" % (100 * math.sqrt(p * (1 - p) / W_BLOCKS)), "%.4f" % binom_flip_prob(p)])
out.append(md(["true signalling share p", "std of the window share", "P(window share >= 95%) on a given day"], rows))
out.append("")
out.append("Reading: a 94.5% fleet never flips by luck (P = 0.0000); a 95.1% fleet flips on its first full day (P = 0.91). A holdout of 5.1% of blue blocks blocks the signal path for ever; only the floor height ends it.")
out.append("")
out.append("## 2. The holdout: what 6 percent of the hash costs to hold until the floor")
out.append("")
out.append("A holdout needs 5% plus one block of the blue blocks of every day until N6. Rented on top of a network of N GH/s that otherwise signals 100%: h/(1 - h) x N for h = 6%.")
out.append("")
rows = []
for N in NETWORKS_GHS:
hr = 0.06 / 0.94 * N
rows.append(["%d GH/s" % N, "%.3f GH/s" % hr, "USD %.0f" % (hr * PRICE_PER_GHS_HOUR * 24), "USD %.0f" % (hr * PRICE_PER_GHS_HOUR * 24 * 14), "USD %.0f" % (hr * PRICE_PER_GHS_HOUR * 24 * 30)])
out.append(md(["network hash", "rented hash for a 6% holdout", "per day", "14 days (a devnet-scale floor)", "30 days"], rows))
out.append("")
out.append("What the holdout gains: nothing but delay, bounded by the floor. The holdout earns block subsidy on its hash like any miner (6% of blocks), so its net cost is the rental premium over revenue, which at a rental market in equilibrium is near zero. The floor is therefore the whole defence against a veto, and the floor is a fixed height: the hazard of 6 October 2026 (the DAA 198,000 crossing while the fleet was still updating: a two-sided chain and a 229-block reorg, CLAUDE.md 6 Oct rules) returns at N6 for any node not yet on the new object.")
out.append("")
out.append("## 3. The renter: forcing the flip early with one day of 95 percent")
out.append("")
out.append("The window is one day of blue blocks and the byte is the template builder's. A renter who patches the byte and holds 95% of a day's blue blocks flips the class at the next epoch boundary whether or not the real fleet has the object. Hash needed: 0.95/0.05 x N = 19 N for 24 h (the window must be full and 95% signalling; a lower share over more days never reaches 95% because the window slides).")
out.append("")
rows = []
for N in NETWORKS_GHS:
hr = 19.0 * N
rows.append(["%d GH/s" % N, "%.0f GH/s" % hr, "USD %.0f" % (hr * PRICE_PER_GHS_HOUR * 24), "%s" % ("rentable tonight (1.75 GH/s on RunPod)" if hr <= 1.75 else "the market could not supply a TH/s on 6 Oct (bench-log)")])
out.append(md(["network hash", "rented hash for one day", "cost of the day", "supply"], rows))
out.append("")
out.append("What it buys: an activation while part of the fleet lacks the object. For the v4 cut nothing: the signalling binary IS the v4 binary, so every node that reads the high byte can mine v4 (section 6.1 item 1), and a pre-signalling node already refuses the high-byte headers. For a later object (v5) it is the fork of the nodes still on v4, the same shape as a fixed height crossed early. Bound: the share of the fleet not on the new object at the forced flip, which the renter chooses. Defence that costs nothing in liveness: require the threshold on each of D consecutive daily windows (D = 7 makes the renter's bill 7x and gives a week of public warning: the chain shows the share every epoch in `programClassV4SignalBps`), or weight the signal by the 30-day finality window, which a one-day rental cannot fill (W2: a day is 1/30 of the table).")
out.append("")
out.append("## 4. Signal then defect")
out.append("")
out.append("A node that signals (byte 4) and after the flip runs the old class produces headers whose PoW fails under the new program: refused by every node, its blocks are lost, nobody else is touched (`check_header_version` then the PoW check). A pool that signals with its template and whose hashers run old workers loses the hashers' blocks after the flip (the worker computes the program from the template's class; a stale worker's blocks fail PoW). Bound: the defector's own income, zero to anyone else. Signal-then-defect is not an attack on the chain; it is a mis-upgraded miner.")
out.append("")
out.append("## 5. The one-day window against the 30-day finality window")
out.append("")
rows = [["one day (P2 as proposed)", "19 N for 24 h", "USD %.0f at 1 GH/s, %.0f at 100 GH/s" % (19 * PRICE_PER_GHS_HOUR * 24, 1900 * PRICE_PER_GHS_HOUR * 24), "flips within a day of the last upgrade", "a renter forces it in a day"],
["7 consecutive days at 95%", "19 N for 7 x 24 h", "USD %.0f at 1 GH/s, %.0f at 100 GH/s" % (19 * PRICE_PER_GHS_HOUR * 24 * 7, 1900 * PRICE_PER_GHS_HOUR * 24 * 7), "flips a week after the last upgrade", "7x the bill and a week of visible share"],
["30-day blue-block window (the finality table)", "the renter must mine 95% of 30 days of blocks: 19 N for 30 days", "USD %.0f at 1 GH/s, %.0f at 100 GH/s" % (19 * PRICE_PER_GHS_HOUR * 24 * 30, 1900 * PRICE_PER_GHS_HOUR * 24 * 30), "flips 30 days after the fleet is ready (the status file's objection)", "same bound as finality's own 2/3: the 20-day public event"]]
out.append(md(["window", "renter's hash", "renter's cost", "honest latency", "what it buys the defence"], rows))
out.append("")
out.append("Recommendation to the lane that owns P2: keep the one-day tally for the live share display, require the threshold on D = 7 consecutive daily windows for the flip, keep the floor as the backstop but set it no nearer than 7 days past the publish on any network miners run (14,400 DAA = 4 h was a devnet value), and never let a floor cross while under 95% is signalling without a node-side line that says which boxes are not on the object (the stale-box list is already a P1 pass rule).")
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())