1 #!/usr/bin/env python3
2 """
3 Limenka drop-and-rise shock simulation.
4 5 Tests the incentive the design claims: when hashrate drops, block
6 interval rises, whoever catches the late block gets the bigger reward
7 plus accumulated fees, and the DAA lowers difficulty until cadence
8 returns toward 600s.
9 10 Runs a configurable drop (hashrate falls to --to-frac for --drop-hours
11 hours, then restores) and reports:
12 13 - the reward paid to the catch block (the first block after the drop)
14 - the actual wait that miner endured vs the e they were paid for
15 - cadence recovery time after restore (3 consecutive intervals within
16 +-50% of target)
17 - issuance vs parity over the whole run
18 - per-block reward time series around the drop (for eyeballing)
19 20 Also compares --ebasis mtp vs direct, because the e measurement changes
21 who gets paid for the late catch (see mtp_sim.py discussion).
22 23 Run: python3 shock_mtp.py --ebasis direct --to-frac 0.1 --drop-hours 24
24 """
25 import argparse
26 import random
27 import sys
28 29 sys.path.insert(0, __import__("os").path.dirname(__file__))
30 import mtp_sim as M
31 32 33 def run(cfg):
34 rng = random.Random(cfg.seed)
35 world = M.World()
36 drop_t = 100 * M.T
37 restore_t = drop_t + cfg.drop_hours * 3600
38 world.const(0, 1.0)
39 world.const(drop_t, cfg.to_frac)
40 world.const(restore_t, 1.0)
41 42 chain = M.Chain(cfg)
43 chain.add_block(0.0, False, seed=True)
44 for _ in range(cfg.blocks):
45 h, a, _mode = world.rates_at(chain.t)
46 total = h + a
47 if total <= 0:
48 break
49 dt = rng.expovariate(total / (M.T * chain.d))
50 arr = chain.t + dt
51 nxt = world.next_boundary(chain.t)
52 if nxt is not None and arr >= nxt:
53 chain.advance_time(nxt)
54 continue
55 chain.add_block(arr, False)
56 57 # catch block: first block arriving after the drop; its wait vs paid e
58 catch = next((b for b in chain.blocks if b[0] >= drop_t), None)
59 if catch is not None:
60 prev_arr = chain.blocks[chain.blocks.index(catch) - 1][0] if chain.blocks.index(catch) > 0 else 0
61 wait = catch[0] - max(prev_arr, drop_t)
62 print(f"catch block: real wait {wait:,.0f}s, paid e {catch[2]:,.0f}s, "
63 f"reward {catch[2] / M.T:.2f} R_full (wait-worth {wait / M.T:.2f} R_full)")
64 65 # largest single reward after the drop (who caught the stall)
66 late = [b for b in chain.blocks if b[0] >= drop_t]
67 big = max(late, key=lambda b: b[2]) if late else None
68 if big is not None:
69 print(f"largest catch reward: e={big[2]:,.0f}s = {big[2] / M.T:.2f} R_full, "
70 f"block size {big[4] / 1e6:,.1f}MB")
71 72 # cadence recovery after restore
73 post = [b for b in chain.blocks if b[0] >= restore_t]
74 restore_hrs = None
75 if post:
76 good = 0
77 for b in post:
78 if abs(b[2] - M.T) <= 0.5 * M.T:
79 good += 1
80 else:
81 good = 0
82 if good >= 3:
83 restore_hrs = (b[0] - restore_t) / 3600
84 break
85 if restore_hrs is not None:
86 print(f"cadence restored {restore_hrs:,.1f}h after hashrate restored")
87 else:
88 print("cadence NOT restored within run")
89 90 ints = sorted(b[2] for b in chain.blocks if b[2] > 0)
91 n = max(1, len(ints))
92 print(f"run: {chain.height} blocks, {chain.t / 86400:.1f} days")
93 print(f"interval mean {sum(ints) / n:,.0f}s median {ints[n // 2]:,.0f}s p90 {ints[int(n * 0.9)]:,.0f}s")
94 print(f"issuance/parity {chain.issuance / (M.R_FULL * chain.t / M.T):.4f}")
95 print(f"difficulty: max {max(b[3] for b in chain.blocks):.3f}, final {chain.d:.3f}")
96 print(f"backlog: max {chain.max_backlog / 1e9:,.2f}GB, max block {chain.max_block_size / 1e6:,.1f}MB")
97 98 # reward time series around the drop: 5 blocks before, 10 after
99 idx = chain.blocks.index(catch) if catch is not None else 0
100 print("\nreward series around drop (e and reward per block):")
101 for b in chain.blocks[max(0, idx - 5):idx + 10]:
102 tag = "CATCH" if b is catch else ""
103 print(f" t={b[0] - drop_t:+8,.0f}s e={b[2]:7,.0f}s reward={b[2] / M.T:6.2f} {tag}")
104 105 106 def main():
107 ap = argparse.ArgumentParser(description="Limenka drop-and-rise shock sim")
108 ap.add_argument("--blocks", type=int, default=9000)
109 ap.add_argument("--seed", type=int, default=42)
110 ap.add_argument("--window", type=int, default=101)
111 ap.add_argument("--future", type=int, default=60)
112 ap.add_argument("--kp", type=float, default=50e6)
113 ap.add_argument("--ki", type=float, default=10e6)
114 ap.add_argument("--kd", type=float, default=0)
115 ap.add_argument("--alpha", type=float, default=0.10)
116 ap.add_argument("--lp", type=float, default=0.15)
117 ap.add_argument("--fill", type=float, default=6600)
118 ap.add_argument("--ebasis", choices=["mtp", "direct"], default="direct")
119 ap.add_argument("--to-frac", type=float, default=0.1)
120 ap.add_argument("--drop-hours", type=float, default=24)
121 cfg = ap.parse_args()
122 cfg.scenario = "steady"
123 cfg.drop = 0.9
124 cfg.flood_x = 50
125 cfg.flood_min = 30
126 cfg.stall_frac = 0.001
127 cfg.stall_days = 4
128 run(cfg)
129 130 131 if __name__ == "__main__":
132 main()
133