169 lines
13 KiB
Python
169 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Node cost, bandwidth, pruning and payout arithmetic per block rate (Horizon network lane, 6 October 2026).
|
|
Every input is labelled in INPUTS below: measured (entry named), cited (file named) or approximate.
|
|
Run: python3 sim/horizon/network/cost.py > sim/horizon/network/cost-tables.md
|
|
"""
|
|
import math
|
|
|
|
def calculate_ghostdag_k(x, delta):
|
|
"""vendor/rusty-kaspa/consensus/core/src/config/bps.rs `calculate_ghostdag_k`, in log space (the f64 form
|
|
underflows exp(-x) at x = 1000 and never terminates; same result where both run: 18 at x 10, 124 at x 100)"""
|
|
k_hat, sigma, lterm = 0, 0.0, -x
|
|
while True:
|
|
sigma += math.exp(lterm)
|
|
if 1.0 - sigma < delta: return k_hat
|
|
k_hat += 1; lterm += math.log(x / k_hat)
|
|
|
|
INPUTS = {
|
|
# cited: vendor/rusty-kaspa consensus/core/src/config/constants.rs and bps.rs
|
|
"delay_bound_s": 5, "delta": 0.01, "merge_depth_s": 3600, "finality_s": 43200, "pruning_s": 108000, "maturity_s": 100,
|
|
# cited: docs/spec/10-light-client.md 10.5 (286 fixed header bytes), 02-consensus.md 2.4
|
|
"header_fixed": 286,
|
|
# simulated: sim/horizon/network/results.md grid, tips mean at hop 300 ms (parents cap 10 at 1 bps, 16 above: bps.rs)
|
|
"parents_mean": {1: 1.4, 10: 4.1, 32: 8.0, 100: 14.2},
|
|
# measured: docs/fud-ledger.md M21 sweep, live devnet p50 block 723 B with 1 coinbase; the coinbase without votes about 300 B (approximate)
|
|
"body_no_votes": 300, "record_bytes": 274, "segment_chain_blocks": 8,
|
|
# cited: fud-close docs/spec/03-finality.md 3.4.2: vote item 281 B, certificate 273 B + V/8, bounds 48 (today) and 384 (proposed)
|
|
"vote_bytes": 281, "cert_fixed": 273, "vote_cap_today": 48, "vote_cap_proposed": 384,
|
|
# cited: spec 03 C1: checkpoint every 30 blue blocks, determination d = 60 blue (placeholder; 20 on devnet)
|
|
"cp_blue": 30, "det_blue": 60,
|
|
# approximate: Kaspa's relay flow (protocol/flows, blockrelay: inv, request, block): per block, degree x 40 B of inv plus one 40 B request
|
|
"degree": 8, "inv_bytes": 40,
|
|
# measured: docs/bench-log.md M30 entry: 256 MiB cache, KEEP_DAYS 3 (768 MiB worst), slope 30.2 MB per 1,000 blocks (s8 steady, narrow DAG);
|
|
# the live app node 1,081 MB at 27 min to 2,258 MB at 4 h 14 min (about 80 KB per block, derived); run A's hub 5.4 GB at 12,000 blocks (440 KB per block, A.jsonl)
|
|
"cache_mib": 256, "keep_days": 3, "rss_per_block_kb": {"narrow (s8 steady)": 30.2, "live devnet app node (derived)": 80, "run A hub, wide DAG, 41 peers": 440},
|
|
# measured: hands-on-build-1.md: exec-snapshot.bin 127,564,588 B at tip 141,700 chain blocks (0.9 KB per chain block); M30 note: ExecState.records 1 to 2 KB per chain block
|
|
"exec_snapshot_bytes": 127564588, "exec_snapshot_tip": 141700, "exec_record_kb": 1.5,
|
|
# measured: lottery verify per header: class v3 2.79 ms on a loaded M5 Max core (consequences C29); class v4 4.9 ms steady / 8.23 ms half-core on the box proxy (lane 2); gate 10 ms (spec 01)
|
|
"verify_ms": {"class v3 (M5 Max loaded core)": 2.79, "class v4 (box proxy)": 4.9, "class v4 half core": 8.23, "gate": 10.0},
|
|
# approximate: one BLS signature verify about 1.5 ms (unmeasured on this stack, O-10.3)
|
|
"bls_verify_ms": 1.5,
|
|
# measured: run A hub CPU per accepted block (A.jsonl cpu_rss deltas over accepted deltas): 61 ms (mergeset about 8), 115 ms (about 30), 345 ms (150 to 200)
|
|
"hub_ms_per_block": {"mergeset 8": 61, "mergeset 30": 115, "mergeset 150 to 200": 345},
|
|
# cited: spec 02 2.5: 31.688 IGN per DAA second at full ramp, 80% to the producer
|
|
"ign_per_s": 31.688, "miner_share": 0.8,
|
|
# the brief's tiers and networks; the rental entry: USD 0.0117 per MH/s-hour (bench-log "Rental cost of hash, 6 October 2026")
|
|
"tiers": {"4070 (25 MH/s)": 25e6, "5090 (128 MH/s)": 128e6, "8x 4090 rig (459 MH/s)": 459e6},
|
|
"networks": {"tonight 1.16 GH/s": 1.16e9, "10 GH/s": 1e10, "100 GH/s": 1e11, "1 TH/s": 1e12, "10 TH/s": 1e13},
|
|
}
|
|
BPS = [1, 10, 32, 100]
|
|
VOTERS = [12, 100, 1000, 8192]
|
|
|
|
def k_of(b): return {1: 18, 10: 124, 32: 362}.get(b) or calculate_ghostdag_k(2 * INPUTS["delay_bound_s"] * b, INPUTS["delta"])
|
|
def parents_cap(k): return max(10, min(16, k // 2))
|
|
def mergeset_cap(k): return max(180, min(512, 2 * k))
|
|
def header_bytes(b): return INPUTS["header_fixed"] + 32 * INPUTS["parents_mean"][b]
|
|
def fmt_bytes(n):
|
|
for u, d in (("TB", 1e12), ("GB", 1e9), ("MB", 1e6), ("KB", 1e3)):
|
|
if n >= d: return f"{n/d:.2f} {u}"
|
|
return f"{n:.0f} B"
|
|
def fmt_time(s):
|
|
if s < 60: return f"{s:.1f} s"
|
|
if s < 3600: return f"{s/60:.1f} min"
|
|
if s < 86400: return f"{s/3600:.1f} h"
|
|
return f"{s/86400:.1f} d"
|
|
|
|
out = []
|
|
def P(s=""): out.append(s)
|
|
|
|
P("# cost.py tables (sim/horizon/network/cost.py). Labels: M = measured, C = cited, S = simulated, A = approximate; sources in the script's INPUTS.")
|
|
P()
|
|
P("## 1. GHOSTDAG parameters per block rate (C: bps.rs formulas; k at 100 bps computed with calculate_ghostdag_k since the table stops at 32)")
|
|
P("| bps | k | max parents | mergeset limit | merge depth, blocks | finality depth, blocks | pruning depth, blocks | coinbase maturity, blocks | checkpoint cadence (30 blue), s | determination d = 60 blue, s | blocks per 30-s window |")
|
|
P("|---|---|---|---|---|---|---|---|---|---|---|")
|
|
for b in BPS:
|
|
k = k_of(b); ms = mergeset_cap(k)
|
|
lower = INPUTS["finality_s"] * b + 2 * INPUTS["merge_depth_s"] * b + 4 * ms * k + 2 * k + 2
|
|
prun = max(lower, INPUTS["pruning_s"] * b)
|
|
P(f"| {b} | {k} | {parents_cap(k)} | {ms} | {INPUTS['merge_depth_s']*b:,} | {INPUTS['finality_s']*b:,} | {prun:,} | {INPUTS['maturity_s']*b:,} | {INPUTS['cp_blue']/b:g} | {INPUTS['det_blue']/b:g} | {30*b} |")
|
|
P()
|
|
P("## 2. Votes and the 3.4.2 bounds per block rate (C: 281 B per vote; checkpoint every 30 BLUE blocks as C1 is written)")
|
|
P("| bps | checkpoints per day | votes per block at V voters (V/30, C1 in blue blocks) | drain capacity per checkpoint, cap 48 / 384 | vote bytes per day, V = 100 / 1,000 / 8,192 (single votes) | the same if C1 were 30 DAA seconds |")
|
|
P("|---|---|---|---|---|---|")
|
|
for b in BPS:
|
|
cps = 86400 * b / INPUTS["cp_blue"]
|
|
vpb = " / ".join(f"{v/30:.1f}" for v in (100, 1000, 8192))
|
|
cap = f"{30*48:,} / {30*384:,}"
|
|
vb = " / ".join(fmt_bytes(v * cps * INPUTS["vote_bytes"]) for v in (100, 1000, 8192))
|
|
vb2 = " / ".join(fmt_bytes(v * 2880 * INPUTS["vote_bytes"]) for v in (100, 1000, 8192))
|
|
P(f"| {b} | {cps:,.0f} | {vpb} | {cap} | {vb} | {vb2} |")
|
|
P()
|
|
P("## 3. Bytes per node per day: receive every block once plus relay overhead (A: Kaspa's inv/request flow, degree 8), votes carried in bodies, one certificate per block")
|
|
P("| bps | header bytes (C+S) | blocks per day | block bytes per day (header + body + records) | relay overhead per day | votes per day at V = 12 / 100 / 1,000 / 8,192 (C1 in blue blocks) | certificates per day at V = 12 / 8,192 | total per day at V = 100 | total at V = 8,192 | Mbit/s mean at V = 8,192 |")
|
|
P("|---|---|---|---|---|---|---|---|---|---|")
|
|
for b in BPS:
|
|
hb = header_bytes(b); n = 86400 * b
|
|
blocks = n * (hb + INPUTS["body_no_votes"] + INPUTS["record_bytes"] / INPUTS["segment_chain_blocks"])
|
|
relay = n * (INPUTS["degree"] * INPUTS["inv_bytes"] + INPUTS["inv_bytes"])
|
|
cps = n / INPUTS["cp_blue"]
|
|
votes = {v: v * cps * INPUTS["vote_bytes"] for v in VOTERS}
|
|
certs = {v: n * (INPUTS["cert_fixed"] + v / 8) for v in VOTERS}
|
|
tot100 = blocks + relay + votes[100] + certs[100]; tot8k = blocks + relay + votes[8192] + certs[8192]
|
|
P(f"| {b} | {hb:.0f} | {n:,} | {fmt_bytes(blocks)} | {fmt_bytes(relay)} | {' / '.join(fmt_bytes(votes[v]) for v in VOTERS)} | {fmt_bytes(certs[12])} / {fmt_bytes(certs[8192])} | {fmt_bytes(tot100)} | {fmt_bytes(tot8k)} | {tot8k*8/86400/1e6:.2f} |")
|
|
P()
|
|
P("With certificates aggregated (lane 3's 1.2 KB per checkpoint) and votes NOT echoed in every body (one carriage), the vote column is the floor: a vote crosses the network once. A star hub multiplies its upload by its peer count: run A's hub sent 2.13 GB in 850 s to 41 peers (A.jsonl rx_tx, netns-wide counter, so an upper bound): 2.5 MB/s, 61 KB/s per peer.")
|
|
P()
|
|
P("## 4. Node CPU per block and the serial budget (the pipeline validates blocks in order; budget = 0.8 core)")
|
|
P("| bps | budget per block, ms | lottery verify class v3 / v4 / v4 half core (M) | BLS for V/30 votes per block at V = 100 / 1,000 / 8,192, cap 384 (A: 1.5 ms per verify) | run A hub measured total at mergeset 8 / 30 / 150+ (M) | fits the budget? |")
|
|
P("|---|---|---|---|---|---|")
|
|
for b in BPS:
|
|
budget = 800 / b
|
|
bls = " / ".join(f"{min(v/30, 384)*INPUTS['bls_verify_ms']:.0f}" for v in (100, 1000, 8192))
|
|
hub = " / ".join(str(v) for v in INPUTS["hub_ms_per_block"].values())
|
|
fits = "yes with margin" if budget > 120 else ("only the verify and a narrow DAG (under 25 ms per block measured nowhere yet)" if budget > 20 else "no: class v4 verify alone (4.9 to 8.2 ms) plus GHOSTDAG exceeds it")
|
|
P(f"| {b} | {budget:.0f} | 2.79 / 4.9 / 8.23 | {bls} | {hub} | {fits} |")
|
|
P()
|
|
P("## 5. RSS and disk per block rate (M: cache 256 MiB x KEEP_DAYS 3; slopes per block from M30 s8, the live app node, run A's hub; all slopes are growth over a run, not a proven steady state)")
|
|
P("| bps | caches | DAG store growth per day at 30 / 80 / 440 KB per block | growth over the 30-h pruning window at 30 / 80 KB per block | exec state (M: 128 MB snapshot at 141,700 chain blocks) | disk per day: blocks + votes at V = 1,000 + exec records |")
|
|
P("|---|---|---|---|---|---|")
|
|
for b in BPS:
|
|
n = 86400 * b
|
|
g = [n * kb * 1e3 for kb in (30.2, 80, 440)]
|
|
w = [INPUTS["pruning_s"] * b * kb * 1e3 for kb in (30.2, 80)]
|
|
hb = header_bytes(b); cps = n / 30
|
|
chain_blocks = n / (1 + INPUTS["parents_mean"][b]) # A: one chain block per mergeset of about 1 + parents blocks
|
|
disk = n * (hb + INPUTS["body_no_votes"]) + 1000 * cps * INPUTS["vote_bytes"] + chain_blocks * INPUTS["exec_record_kb"] * 1e3
|
|
P(f"| {b} | 768 MiB worst, 256 MiB flat | {' / '.join(fmt_bytes(x) for x in g)} | {' / '.join(fmt_bytes(x) for x in w)} | 128 MB + 1.5 KB per chain block | {fmt_bytes(disk)} |")
|
|
P()
|
|
P("## 6. Pruned and archival storage per year (C: pruning window 108,000 DAA s; A: 2x index overhead on blocks; votes single, one carriage)")
|
|
P("| bps | pruned node keeps (window of headers + bodies, 2x) | plus votes in the window at V = 1,000 | archival per year: blocks (2x) | archival votes per year at V = 100 / 1,000 / 8,192 (C1 in blue blocks) | archival votes per year if certificates only (1.2 KB per checkpoint, lane 3) |")
|
|
P("|---|---|---|---|---|---|")
|
|
for b in BPS:
|
|
hb = header_bytes(b); win = INPUTS["pruning_s"] * b
|
|
kept = 2 * win * (hb + INPUTS["body_no_votes"]); kv = 1000 * (win / 30) * INPUTS["vote_bytes"]
|
|
yr = 31557600 * b
|
|
arch = 2 * yr * (hb + INPUTS["body_no_votes"]); cps_y = yr / 30
|
|
av = " / ".join(fmt_bytes(v * cps_y * INPUTS["vote_bytes"]) for v in (100, 1000, 8192))
|
|
P(f"| {b} | {fmt_bytes(kept)} | {fmt_bytes(kv)} | {fmt_bytes(arch)} | {av} | {fmt_bytes(cps_y * 1200)} |")
|
|
P()
|
|
P("## 7. Subsidy per block and payout intervals (C: 31.688 IGN per DAA s at full ramp, 80% to the producer; interval = 1 / (bps x share))")
|
|
P("| bps | subsidy per block, IGN | miner's 80%, IGN | " + " | ".join(f"{t} at {nname}" for nname in INPUTS["networks"] for t in ("4070", "5090", "rig")) + " |")
|
|
P("|---|---|---|" + "---|" * (3 * len(INPUTS["networks"])))
|
|
for b in BPS:
|
|
sub = INPUTS["ign_per_s"] / b
|
|
cells = []
|
|
for nname, nh in INPUTS["networks"].items():
|
|
for t, h in INPUTS["tiers"].items():
|
|
cells.append(fmt_time(1 / (b * h / nh)))
|
|
P(f"| {b} | {sub:.3f} | {sub*INPUTS['miner_share']:.3f} | " + " | ".join(cells) + " |")
|
|
P()
|
|
P("## 8. The solo-miner question: a 4070 at 25 MH/s, blocks per day and the rate needed for one block a day")
|
|
P("| network hash | share | blocks per day at 1 bps | bps for one block a day | IGN per day at 1 bps (full ramp, 80%) |")
|
|
P("|---|---|---|---|---|")
|
|
for nname, nh in INPUTS["networks"].items():
|
|
share = 25e6 / nh; bpd = 86400 * share
|
|
P(f"| {nname} | {share:.2e} | {bpd:.2f} | {1/bpd:.4f} | {bpd * INPUTS['ign_per_s'] * INPUTS['miner_share']:,.1f} |")
|
|
P()
|
|
P("## 9. Finality timing per block rate if C1 and d stay in blue blocks (C: spec 03 C1; the lock about 3 s after determination, lane 4's reading of 3.11.3)")
|
|
P("| bps | checkpoint cadence, s | determination after the checkpoint, s | lock after the checkpoint, s | checkpoints per presence window of 240 indices, minutes | 30-day window in blue blocks |")
|
|
P("|---|---|---|---|---|---|")
|
|
for b in BPS:
|
|
P(f"| {b} | {30/b:g} | {60/b:g} | {60/b + 3:g} | {240*30/b/60:g} | {2592000*b:,} |")
|
|
P()
|
|
P("## 10. Light client bytes per day (C: spec 10.5 shape; checkpoint mode 2,880 x (header 400 + certificate + proof 400) at 1 bps; scales with checkpoints per day if C1 stays in blue blocks)")
|
|
P("| bps | checkpoints per day | checkpoint mode at 1,000 voters | at 10,000 voters | phase two (800 B per checkpoint) |")
|
|
P("|---|---|---|---|---|")
|
|
for b in BPS:
|
|
cps = 2880 * b
|
|
P(f"| {b} | {cps:,} | {fmt_bytes(cps * (400 + 273 + 125 + 400))} | {fmt_bytes(cps * (400 + 273 + 1250 + 400))} | {fmt_bytes(cps * 800)} |")
|
|
print("\n".join(out))
|