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