94 lines
5.4 KiB
Python
94 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""The difficulty controller against a wide DAG: the coupling between red share, chain-step spacing and the estimator
|
|
(Horizon network lane, 6 October 2026).
|
|
|
|
The rule as the fork runs it (vendor/igneum-node release-0.3.14-node, consensus/src/processes/difficulty.rs
|
|
`igneum_difficulty_bits`, consensus/core/src/igneum.rs `difficulty`): the lanes walk the SELECTED CHAIN; every step
|
|
carries `work = blue_work(b) - blue_work(selected parent)` (the mergeset BLUES' work only, red work is not in blue
|
|
work) and `solvetime = min(c(b) - c(p), CAP_BLOCKS x T)` with CAP_BLOCKS = 20 (2 s at T = 100 ms); the target moves at
|
|
most HARDEN_PERCENT 3 (difficulty up) or EASE_PERCENT 10 (difficulty down) per chain block (spec 02 2.3).
|
|
Kaspa's rule (vendor/rusty-kaspa consensus/src/processes/window.rs `push_mergeset`, difficulty.rs
|
|
`calculate_difficulty_bits`): the DAA window holds every mergeset block, blue AND red, above the window's blue-score
|
|
floor, and the new target is the window's average target x measured span / (window size x T): the whole-DAG rate
|
|
over the real span.
|
|
|
|
The abstract DAG regime (no block-level simulation; the inputs are the propagation lane's):
|
|
spacing = max(1 / lambda, d) seconds between selected-chain blocks (every block a chain block when the DAG is
|
|
narrow; one chain block per propagation delay when it is wide)
|
|
m = lambda x spacing mergeset per chain block
|
|
blues = min(m, k + 1) the k-cluster admits at most k blues beside the selected parent per chain step
|
|
(protocol.rs); the rest of the mergeset is red: r = 1 - blues / m
|
|
(the PHANTOM tail P(Poisson(2 d lambda) > k) that k is derived from, bps.rs `calculate_ghostdag_k`, says when
|
|
reds appear at all; it is printed beside r)
|
|
Estimators of the network hash H (work per block = D, the difficulty):
|
|
igneum = blues x D / min(spacing, cap) (blue work over the capped chain step)
|
|
whole = m x D / spacing (every mergeset block's work over the real span) = H
|
|
blue-rc = blues x D / spacing / (1 - r_obs) (blue rate with the observed red share corrected) = H
|
|
The controller sets the next D toward estimate x T through the 3% / 10% clamps once per chain block.
|
|
|
|
Run: python3 sim/horizon/network/controller.py [--bps 10 --k 124 --delays 0.3,1,2,5,10 --minutes 30 --lambda0 26]
|
|
"""
|
|
import argparse, math
|
|
|
|
def pois_tail_gt(k, x):
|
|
"""P(Poisson(x) > k)"""
|
|
if x <= 0: return 0.0
|
|
# sum P(N <= k) in log space
|
|
lp = -x; s = math.exp(lp)
|
|
for i in range(1, k + 1):
|
|
lp += math.log(x / i); s += math.exp(lp)
|
|
if lp < -745: break
|
|
return max(0.0, min(1.0, 1.0 - s))
|
|
|
|
def run(est, bps, k, d, minutes, lam0, H=1e9):
|
|
T = 1.0 / bps; cap = 20 * T
|
|
D = H / lam0 # difficulty giving the initial production rate
|
|
t = 0.0; rows = []
|
|
while t < minutes * 60:
|
|
lam = H / D
|
|
x = 2 * d * lam
|
|
spacing = max(1.0 / lam, d)
|
|
m = lam * spacing
|
|
blues = min(m, k + 1)
|
|
r = 1 - blues / m
|
|
if est == "igneum":
|
|
h = blues * D / min(spacing, cap)
|
|
elif est == "whole":
|
|
h = m * D / spacing
|
|
else:
|
|
h = blues * D / spacing / max(1e-9, 1 - r)
|
|
want = h * T
|
|
D_new = min(D * 1.03, max(D * 0.90, want)) # harden at most 3%, ease at most 10%
|
|
rows.append((t, lam, r, m, blues, h / H, D, pois_tail_gt(k, x)))
|
|
D = D_new; t += spacing
|
|
return rows
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--bps", type=float, default=10); ap.add_argument("--k", type=int, default=124)
|
|
ap.add_argument("--delays", default="0.3,1,2,5,10"); ap.add_argument("--minutes", type=float, default=30)
|
|
ap.add_argument("--lambda0", type=float, default=26.0, help="initial production rate, blocks/s (run A's genesis burst was 26)")
|
|
ap.add_argument("--out", default=None)
|
|
a = ap.parse_args()
|
|
lines = []
|
|
def emit(s): print(s); lines.append(s)
|
|
emit(f"# controller.py: bps {a.bps:g}, k {a.k}, cap {20/a.bps:g} s, start at {a.lambda0:g} blocks/s, {a.minutes:g} min; settled = mean of the last 5 minutes")
|
|
emit("")
|
|
emit("| estimator | delay d, s | DAG rate settled, blocks/s | blue rate, blocks/s | red share | P(anticone > k) | mergeset per chain block | estimate / true hash | difficulty vs correct | time to within 10% of target DAG rate, min |")
|
|
emit("|---|---|---|---|---|---|---|---|---|---|")
|
|
for est in ("igneum", "whole", "blue-rc"):
|
|
for d in [float(v) for v in a.delays.split(",")]:
|
|
rows = run(est, a.bps, a.k, d, a.minutes, a.lambda0)
|
|
tail = [r for r in rows if r[0] >= (a.minutes - 5) * 60] or rows[-5:]
|
|
lam = sum(r[1] for r in tail) / len(tail); red = sum(r[2] for r in tail) / len(tail)
|
|
m = sum(r[3] for r in tail) / len(tail); ratio = sum(r[5] for r in tail) / len(tail)
|
|
Dcorrect = 1e9 / a.bps; Dratio = sum(r[6] for r in tail) / len(tail) / Dcorrect
|
|
within = next((r[0] / 60 for r in rows if abs(r[1] - a.bps) <= 0.1 * a.bps), None)
|
|
wtxt = f"{within:.1f}" if within is not None else "never"
|
|
tail_p = sum(r[7] for r in tail) / len(tail)
|
|
emit(f"| {est} | {d:g} | {lam:.2f} | {lam*(1-red):.2f} | {100*red:.1f}% | {tail_p:.2f} | {m:.1f} | {ratio:.2f}x | {Dratio:.2f}x | {wtxt} |")
|
|
if a.out:
|
|
with open(a.out, "w") as f: f.write("\n".join(lines) + "\n")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|