Status lines updated from the bench-log, sim/results_v2.md, sim/difficulty/attacks and sim/economy where the evidence existed and the ledger still said Open (M15, M17, M19, M24 fixed and live; F1, F7, F19, E12, E15 answered with evidence; M26, M27, X21 fix built on miner-reliability; the rest annotated with what the experiment needs). New: sim/economy/security_budget.py (E15 fee grid, price paths, hashrate response), fud-fixes section 2.5 (rows 117 to 126), docs/review/ledger-sweep-2026-10-05.md (the running table). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
112 lines
6.2 KiB
Python
112 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Security budget through the halvings (ledger E15, open item O-5.11).
|
|
|
|
Arithmetic on the emission schedule of spec 2.5 against a fee grid, a price path and an electricity price, reporting
|
|
per year what reaches miners and provers apart from the burn, the fleet that income supports, and what a one-day 51%
|
|
rental and a 20-day two-thirds campaign cost against that fleet. Extends docs/analysis/security-budget.md (3 October
|
|
2026), which has the emission table and the USD 1,000,000 floor; this script adds the fee grid, the price paths and the
|
|
hashrate response the ledger asked for. Nothing here is a prediction: every price and fee is an input, labelled.
|
|
|
|
python3 security_budget.py the full grid as markdown
|
|
python3 security_budget.py --elec 0.08 another electricity price, USD per kWh
|
|
python3 security_budget.py --card-w 300 --card-mh 124
|
|
|
|
Model. Emission: 1,000,000,000 IGN per 365.25-day year for years 1 and 2 (year 1 minus the 30-day ramp, about 37
|
|
million IGN), halving every 63,115,200 DAA seconds (two years), 80% to the block producer and 20% to the proving pool
|
|
(spec 2.5). Fees: a priority-fee grid in IGN per block (low 0.01, medium 0.1, high 1.0), 80% of which reaches miners
|
|
and provers (spec 5.2; the burned base fee reaches nobody and is not counted); external proving demand in USD per year
|
|
(zero, or the economy simulator's launch assumption of USD 2,000 a day, `sim/economy/sim.py` ext_usd_day), 90% to
|
|
provers (spec 5.4). Price paths: flat at USD 0.005, 0.02 and 0.10 per IGN (the three inputs of the 3 October analysis),
|
|
and from the base input a falling path (-30% a year) and a rising path (+30% a year). Hashrate response: the fleet is
|
|
the number of cards whose electricity the miners' income pays for at the stated price, one card at `--card-w` watts and
|
|
`--card-mh` MH/s all year (an operator at break-even on power; capital, rent and margin are zero, so the fleet is an upper
|
|
bound). Rental cost: a 51% attacker for one day matches that fleet for 24 h at the same electricity price; the 20-day
|
|
figure is the finality headline (20 days of 100% of hashrate to hold two thirds of weight, spec 3). The floor is the
|
|
analysis's USD 1,000,000 a year to miners.
|
|
"""
|
|
import argparse
|
|
|
|
YEAR_S = 365.25 * 86400
|
|
HALVING_S = 63_115_200
|
|
RAMP_LOSS = 37_000_000 # IGN never minted in the 30-day ramp, spec 2.5 (approximate, from the 3 October analysis)
|
|
BLOCKS_PER_YEAR = YEAR_S # one DAA second per block at 1 block/s; the fee grid is per block
|
|
|
|
|
|
def emission(year):
|
|
"""IGN emitted in calendar year `year` (1-based)."""
|
|
halvings = (year - 1) // 2
|
|
e = 1_000_000_000 / (2 ** halvings)
|
|
if year == 1:
|
|
e -= RAMP_LOSS
|
|
return e
|
|
|
|
|
|
def price_path(kind, year):
|
|
if kind == "flat-low":
|
|
return 0.005
|
|
if kind == "flat-base":
|
|
return 0.02
|
|
if kind == "flat-high":
|
|
return 0.10
|
|
if kind == "falling":
|
|
return 0.02 * (0.7 ** (year - 1))
|
|
if kind == "rising":
|
|
return 0.02 * (1.3 ** (year - 1))
|
|
raise ValueError(kind)
|
|
|
|
|
|
def row(year, price, tip_per_block, ext_usd, elec, card_w, card_mh):
|
|
e = emission(year)
|
|
miners_ign = 0.8 * e + 0.8 * tip_per_block * BLOCKS_PER_YEAR * 0.5 # the 80% tip share split miners/provers, 50/50 assumed
|
|
provers_ign = 0.2 * e + 0.8 * tip_per_block * BLOCKS_PER_YEAR * 0.5
|
|
miners_usd = miners_ign * price
|
|
provers_usd = provers_ign * price + 0.9 * ext_usd
|
|
burn_usd = 0.1 * ext_usd # the job burn only; the base fee is outside the grid and the unregistered app share is ignored
|
|
card_year_usd = card_w / 1000 * 8766 * elec
|
|
cards = miners_usd / card_year_usd
|
|
fleet_mh = cards * card_mh
|
|
rent_day = cards * card_w / 1000 * 24 * elec
|
|
return dict(year=year, price=price, miners=miners_usd, provers=provers_usd, burn=burn_usd, cards=cards,
|
|
fleet_mh=fleet_mh, rent_day=rent_day, rent_20d=20 * rent_day)
|
|
|
|
|
|
def fmt_usd(x):
|
|
return f"{x:,.0f}"
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--elec", type=float, default=0.12, help="USD per kWh")
|
|
ap.add_argument("--card-w", type=float, default=300.0)
|
|
ap.add_argument("--card-mh", type=float, default=124.0, help="MH/s per card (RTX 5090 on the devnet, 4 Oct 2026)")
|
|
ap.add_argument("--years", type=int, default=14)
|
|
ap.add_argument("--ext-usd", type=float, default=2000 * 365.25, help="external proving demand, USD per year, in the demand columns")
|
|
a = ap.parse_args()
|
|
|
|
print(f"# Security budget through the halvings (E15 / O-5.11), electricity USD {a.elec}/kWh, card {a.card_w:.0f} W at {a.card_mh:.0f} MH/s\n")
|
|
print("Every price and fee is an input. Fleet = cards whose power the miners' income pays for (upper bound). "
|
|
"Rent 1 d = electricity for a fleet of equal size for 24 h (a 51% attacker at power cost); rent 20 d = the two-thirds campaign.\n")
|
|
|
|
grids = [("no demand, no tips", 0.0, 0.0), ("low tips 0.01 IGN/block, no demand", 0.01, 0.0),
|
|
("medium tips 0.1 IGN/block, launch demand", 0.1, a.ext_usd), ("high tips 1 IGN/block, launch demand", 1.0, a.ext_usd)]
|
|
paths = ["flat-low", "flat-base", "flat-high", "falling", "rising"]
|
|
|
|
for label, tip, ext in grids:
|
|
print(f"## Fee grid: {label}\n")
|
|
for path in paths:
|
|
print(f"### Price path {path}\n")
|
|
print("| year | price USD | to miners USD | to provers USD | burned USD (job share) | fleet cards | fleet MH/s | rent 1 d USD | rent 20 d USD | below floor |")
|
|
print("|---|---|---|---|---|---|---|---|---|---|")
|
|
first_below = None
|
|
for y in range(1, a.years + 1):
|
|
r = row(y, price_path(path, y), tip, ext, a.elec, a.card_w, a.card_mh)
|
|
below = r["miners"] < 1_000_000
|
|
if below and first_below is None:
|
|
first_below = y
|
|
print(f"| {y} | {r['price']:.4f} | {fmt_usd(r['miners'])} | {fmt_usd(r['provers'])} | {fmt_usd(r['burn'])} | {r['cards']:,.0f} | "
|
|
f"{r['fleet_mh']:,.0f} | {fmt_usd(r['rent_day'])} | {fmt_usd(r['rent_20d'])} | {'yes' if below else ''} |")
|
|
print(f"\nFirst year below the USD 1,000,000 miner floor: {first_below if first_below else 'none within ' + str(a.years) + ' years'}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|