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