shock_mtp.py raw

   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