#!/usr/bin/env python3 """ Limenka drop-and-rise shock simulation. Tests the incentive the design claims: when hashrate drops, block interval rises, whoever catches the late block gets the bigger reward plus accumulated fees, and the DAA lowers difficulty until cadence returns toward 600s. Runs a configurable drop (hashrate falls to --to-frac for --drop-hours hours, then restores) and reports: - the reward paid to the catch block (the first block after the drop) - the actual wait that miner endured vs the e they were paid for - cadence recovery time after restore (3 consecutive intervals within +-50% of target) - issuance vs parity over the whole run - per-block reward time series around the drop (for eyeballing) Also compares --ebasis mtp vs direct, because the e measurement changes who gets paid for the late catch (see mtp_sim.py discussion). Run: python3 shock_mtp.py --ebasis direct --to-frac 0.1 --drop-hours 24 """ import argparse import random import sys sys.path.insert(0, __import__("os").path.dirname(__file__)) import mtp_sim as M def run(cfg): rng = random.Random(cfg.seed) world = M.World() drop_t = 100 * M.T restore_t = drop_t + cfg.drop_hours * 3600 world.const(0, 1.0) world.const(drop_t, cfg.to_frac) world.const(restore_t, 1.0) chain = M.Chain(cfg) chain.add_block(0.0, False, seed=True) for _ in range(cfg.blocks): h, a, _mode = world.rates_at(chain.t) total = h + a if total <= 0: break dt = rng.expovariate(total / (M.T * chain.d)) arr = chain.t + dt nxt = world.next_boundary(chain.t) if nxt is not None and arr >= nxt: chain.advance_time(nxt) continue chain.add_block(arr, False) # catch block: first block arriving after the drop; its wait vs paid e catch = next((b for b in chain.blocks if b[0] >= drop_t), None) if catch is not None: prev_arr = chain.blocks[chain.blocks.index(catch) - 1][0] if chain.blocks.index(catch) > 0 else 0 wait = catch[0] - max(prev_arr, drop_t) print(f"catch block: real wait {wait:,.0f}s, paid e {catch[2]:,.0f}s, " f"reward {catch[2] / M.T:.2f} R_full (wait-worth {wait / M.T:.2f} R_full)") # largest single reward after the drop (who caught the stall) late = [b for b in chain.blocks if b[0] >= drop_t] big = max(late, key=lambda b: b[2]) if late else None if big is not None: print(f"largest catch reward: e={big[2]:,.0f}s = {big[2] / M.T:.2f} R_full, " f"block size {big[4] / 1e6:,.1f}MB") # cadence recovery after restore post = [b for b in chain.blocks if b[0] >= restore_t] restore_hrs = None if post: good = 0 for b in post: if abs(b[2] - M.T) <= 0.5 * M.T: good += 1 else: good = 0 if good >= 3: restore_hrs = (b[0] - restore_t) / 3600 break if restore_hrs is not None: print(f"cadence restored {restore_hrs:,.1f}h after hashrate restored") else: print("cadence NOT restored within run") ints = sorted(b[2] for b in chain.blocks if b[2] > 0) n = max(1, len(ints)) print(f"run: {chain.height} blocks, {chain.t / 86400:.1f} days") print(f"interval mean {sum(ints) / n:,.0f}s median {ints[n // 2]:,.0f}s p90 {ints[int(n * 0.9)]:,.0f}s") print(f"issuance/parity {chain.issuance / (M.R_FULL * chain.t / M.T):.4f}") print(f"difficulty: max {max(b[3] for b in chain.blocks):.3f}, final {chain.d:.3f}") print(f"backlog: max {chain.max_backlog / 1e9:,.2f}GB, max block {chain.max_block_size / 1e6:,.1f}MB") # reward time series around the drop: 5 blocks before, 10 after idx = chain.blocks.index(catch) if catch is not None else 0 print("\nreward series around drop (e and reward per block):") for b in chain.blocks[max(0, idx - 5):idx + 10]: tag = "CATCH" if b is catch else "" print(f" t={b[0] - drop_t:+8,.0f}s e={b[2]:7,.0f}s reward={b[2] / M.T:6.2f} {tag}") def main(): ap = argparse.ArgumentParser(description="Limenka drop-and-rise shock sim") ap.add_argument("--blocks", type=int, default=9000) ap.add_argument("--seed", type=int, default=42) ap.add_argument("--window", type=int, default=101) ap.add_argument("--future", type=int, default=60) ap.add_argument("--kp", type=float, default=50e6) ap.add_argument("--ki", type=float, default=10e6) ap.add_argument("--kd", type=float, default=0) ap.add_argument("--alpha", type=float, default=0.10) ap.add_argument("--lp", type=float, default=0.15) ap.add_argument("--fill", type=float, default=6600) ap.add_argument("--ebasis", choices=["mtp", "direct"], default="direct") ap.add_argument("--to-frac", type=float, default=0.1) ap.add_argument("--drop-hours", type=float, default=24) cfg = ap.parse_args() cfg.scenario = "steady" cfg.drop = 0.9 cfg.flood_x = 50 cfg.flood_min = 30 cfg.stall_frac = 0.001 cfg.stall_days = 4 run(cfg) if __name__ == "__main__": main()