handoff_sim.py raw

   1  #!/usr/bin/env python3
   2  """
   3  Activation handoff simulation.
   4  
   5  Simulates the fork boundary: a parent chain (mainnet rules: stamps >
   6  MTP-11 floor, <= now+2h, no monotonicity, no delay) running at 600s
   7  cadence, then the fork activating per the chosen gate.
   8  
   9  Measures:
  10    - activation lag: wall time of the first fork block vs T_act
  11      (the MTP-101 median sits ~50 blocks back, so the gate fires late)
  12    - first-fork-block e under parent stamp variance (parent stamps can
  13      legally sit up to +2h ahead of wall clock)
  14    - the transition rule: fork block 1 stamps within +60s of now even
  15      if that breaks monotonicity with the parent's last stamp (no 2h
  16      wait); subsequent fork blocks use full monotonicity
  17    - PID warmup: cadence convergence in the first 1000 fork blocks,
  18      with and without seeding the EMA from the parent's cadence
  19    - issuance parity in the first 2000 fork blocks
  20  
  21  Gate variants under test:
  22    - mtp101:   MTP-101(pindexPrev) >= T_act           (settled)
  23    - last:     last parent stamp >= T_act             (bias candidate)
  24    - mtp-bias: MTP-101(pindexPrev) + 50*600 >= T_act  (bias candidate)
  25  
  26  Run:  python3 handoff_sim.py
  27  """
  28  import bisect
  29  import math
  30  import os
  31  import random
  32  import sys
  33  
  34  sys.path.insert(0, os.path.dirname(__file__))
  35  import mtp_sim as M
  36  
  37  
  38  class Cfg:
  39      def __init__(self, **kw):
  40          self.window = 101
  41          self.future = 60
  42          self.kp = 10e6
  43          self.ki = 10e6
  44          self.kd = 0
  45          self.ki_up = 5e6
  46          self.ki_down = 50e6
  47          self.alpha = 0.05
  48          self.lp = 0.15
  49          self.target = 617.0
  50          self.fill = 6600
  51          self.ebasis = "direct"
  52          self.monotonic = True
  53          self.delay = 60.0
  54          self.att_hw = 1.0
  55          self.prop_base = 0.0
  56          self.bandwidth = 100e6
  57          self.honest_miners = 1
  58          self.seed = 7
  59          self.scenario = "steady"
  60          self.blocks = 2000
  61          self.drop = 0.9
  62          self.ramp_x = 10.0
  63          self.flood_x = 50
  64          self.flood_min = 30
  65          self.stall_frac = 0.001
  66          self.stall_days = 4
  67          self.attack_share = 0.6
  68          for k, v in kw.items():
  69              setattr(self, k, v)
  70  
  71  
  72  class ParentChain:
  73      def __init__(self, seed=42):
  74          self.rng = random.Random(seed)
  75          self.stamps = []
  76          self.wall = 0.0
  77  
  78      def mtp11(self):
  79          if len(self.stamps) < 11:
  80              return self.stamps[0] if self.stamps else 0
  81          return sorted(self.stamps[-11:])[5]
  82  
  83      def mtp101(self):
  84          if len(self.stamps) < 101:
  85              return sorted(self.stamps)[len(self.stamps) // 2] if self.stamps else 0
  86          return sorted(self.stamps[-101:])[50]
  87  
  88      def add_block(self):
  89          self.wall += self.rng.expovariate(1.0 / 600.0)
  90          floor = self.mtp11() + 1
  91          stamp = min(self.wall + 7200, max(self.wall, floor))
  92          self.stamps.append(stamp)
  93          return stamp
  94  
  95  
  96  def run_handoff(gate="mtp101", seed_parent=42, seed_fork=7, bias_blocks=50,
  97                  seed_ema=True, parent_blocks=3000, fork_blocks=2000,
  98                  parent_stamp_ahead=False):
  99      parent = ParentChain(seed_parent)
 100      for _ in range(parent_blocks):
 101          parent.add_block()
 102      t_act = parent.wall + 1200.0
 103      while True:
 104          parent.add_block()
 105          mtp101 = parent.mtp101()
 106          last = parent.stamps[-1]
 107          gate_ok = {"mtp101": mtp101 >= t_act,
 108                     "last": last >= t_act,
 109                     "mtp-bias": mtp101 + bias_blocks * 600.0 >= t_act}[gate]
 110          if gate_ok:
 111              break
 112      fork_act_wall = parent.wall
 113      parent_last_stamp = parent.stamps[-1]
 114      if parent_stamp_ahead:
 115          parent_last_stamp = parent.wall + 7200
 116  
 117      cfg = Cfg()
 118      chain = M.Chain(cfg)
 119      for s in parent.stamps[-101:]:
 120          bisect.insort(chain.mtp_sorted, s)
 121          chain.mtp_deque.append(s)
 122      chain.last_block_t = parent.wall
 123      chain.t = parent.wall
 124      chain.height = parent_blocks
 125  
 126      parent_mean_int = (parent.stamps[-1] - parent.stamps[-101]) / 100.0
 127      if seed_ema:
 128          chain.avg_error = cfg.target - parent_mean_int
 129          chain.err_lp = chain.avg_error
 130          chain.err_lp_prev = chain.avg_error
 131  
 132      rng = random.Random(seed_fork)
 133      first_e = None
 134      first_wait_s = None
 135  
 136      for i in range(fork_blocks):
 137          dt = rng.expovariate(1.0 / (M.T * chain.d))
 138          arr = chain.t + dt
 139          min_arr = chain.last_block_t + chain.delay
 140          if arr < min_arr:
 141              arr = min_arr
 142          if i == 0:
 143              # transition rule: fork block 1 stamps within +60s of now,
 144              # breaking monotonicity with the parent stamp if needed
 145              mtp_prev = chain.mtp()
 146              stamp = min(arr + cfg.future, max(arr, mtp_prev + 1))
 147              if stamp < mtp_prev + 1:
 148                  stamp = mtp_prev + 1
 149              # the wait that FULL monotonicity would have imposed:
 150              full_floor = parent_last_stamp + 1
 151              first_wait_s = max(0.0, full_floor - (arr + cfg.future))
 152              # commit the first block by hand
 153              chain.advance_time(arr)
 154              chain.last_block_t = arr
 155              chain.push_stamp(stamp)
 156              e = max(1, stamp - parent_last_stamp)
 157              reward = M.R_FULL * e / M.T
 158              allowance = M.S_MAX * e / M.T
 159              size = min(allowance, chain.backlog)
 160              chain.backlog = max(0.0, chain.backlog - size)
 161              chain.max_block_size = max(chain.max_block_size, size)
 162              chain.issuance += reward
 163              chain.honest_reward += reward
 164              chain.height += 1
 165              chain.blocks.append((arr, False, e, chain.d, size))
 166              first_e = e
 167              continue
 168          chain.add_block(arr, False, mode="honest")
 169  
 170      ints = [b[2] for b in chain.blocks if b[2] > 0]
 171      warmup = ints[:100]
 172      warmup_mean = sum(warmup) / len(warmup) if warmup else 0
 173      later = ints[200:]
 174      later_mean = sum(later) / len(later) if later else 0
 175      parity = chain.issuance / (M.R_FULL * (chain.t - fork_act_wall) / M.T) if chain.t > fork_act_wall else 0
 176      return {
 177          "gate": gate,
 178          "lag_h": (fork_act_wall - t_act) / 3600.0,
 179          "first_e": first_e,
 180          "first_wait_s": first_wait_s,
 181          "warmup_mean": warmup_mean,
 182          "later_mean": later_mean,
 183          "parity": parity,
 184          "d_final": chain.d,
 185      }
 186  
 187  
 188  def main():
 189      print("activation handoff: gate variants")
 190      print(f"{'gate':10s} {'lag_h':>8s} {'warmup100':>10s} {'later_mean':>11s} "
 191            f"{'parity':>8s} {'d_fin':>7s} {'first_e':>8s}")
 192      for gate in ("mtp101", "last", "mtp-bias"):
 193          r = run_handoff(gate=gate)
 194          print(f"{gate:10s} {r['lag_h']:8.2f} {r['warmup_mean']:10.1f} "
 195                f"{r['later_mean']:11.1f} {r['parity']:8.4f} {r["d_final"]:7.3f} "
 196                f"{r['first_e']:8.1f}")
 197          if r["first_wait_s"] is not None:
 198              print(f"{'':10s} full-monotonic wait avoided: {r['first_wait_s']:,.0f}s")
 199  
 200      print("\nEMA seeding (gate mtp101):")
 201      print(f"{'seed_ema':>9s} {'warmup100':>10s} {'later_mean':>11s} {'parity':>8s}")
 202      for seed_ema in (True, False):
 203          r = run_handoff(gate="mtp101", seed_ema=seed_ema)
 204          print(f"{str(seed_ema):>9s} {r['warmup_mean']:10.1f} "
 205                f"{r['later_mean']:11.1f} {r['parity']:8.4f}")
 206  
 207      print("\nadversarial parent stamp (+2h ahead):")
 208      for gate in ("mtp101", "last"):
 209          r = run_handoff(gate=gate, parent_stamp_ahead=True)
 210          print(f"{gate:10s} lag_h={r['lag_h']:.2f} first_e={r['first_e']:.1f} "
 211                f"wait_avoided={r['first_wait_s']:,.0f}s warmup={r['warmup_mean']:.1f}s")
 212  
 213  
 214  if __name__ == "__main__":
 215      main()
 216