#!/usr/bin/env python3 """ Activation handoff simulation. Simulates the fork boundary: a parent chain (mainnet rules: stamps > MTP-11 floor, <= now+2h, no monotonicity, no delay) running at 600s cadence, then the fork activating per the chosen gate. Measures: - activation lag: wall time of the first fork block vs T_act (the MTP-101 median sits ~50 blocks back, so the gate fires late) - first-fork-block e under parent stamp variance (parent stamps can legally sit up to +2h ahead of wall clock) - the transition rule: fork block 1 stamps within +60s of now even if that breaks monotonicity with the parent's last stamp (no 2h wait); subsequent fork blocks use full monotonicity - PID warmup: cadence convergence in the first 1000 fork blocks, with and without seeding the EMA from the parent's cadence - issuance parity in the first 2000 fork blocks Gate variants under test: - mtp101: MTP-101(pindexPrev) >= T_act (settled) - last: last parent stamp >= T_act (bias candidate) - mtp-bias: MTP-101(pindexPrev) + 50*600 >= T_act (bias candidate) Run: python3 handoff_sim.py """ import bisect import math import os import random import sys sys.path.insert(0, os.path.dirname(__file__)) import mtp_sim as M class Cfg: def __init__(self, **kw): self.window = 101 self.future = 60 self.kp = 10e6 self.ki = 10e6 self.kd = 0 self.ki_up = 5e6 self.ki_down = 50e6 self.alpha = 0.05 self.lp = 0.15 self.target = 617.0 self.fill = 6600 self.ebasis = "direct" self.monotonic = True self.delay = 60.0 self.att_hw = 1.0 self.prop_base = 0.0 self.bandwidth = 100e6 self.honest_miners = 1 self.seed = 7 self.scenario = "steady" self.blocks = 2000 self.drop = 0.9 self.ramp_x = 10.0 self.flood_x = 50 self.flood_min = 30 self.stall_frac = 0.001 self.stall_days = 4 self.attack_share = 0.6 for k, v in kw.items(): setattr(self, k, v) class ParentChain: def __init__(self, seed=42): self.rng = random.Random(seed) self.stamps = [] self.wall = 0.0 def mtp11(self): if len(self.stamps) < 11: return self.stamps[0] if self.stamps else 0 return sorted(self.stamps[-11:])[5] def mtp101(self): if len(self.stamps) < 101: return sorted(self.stamps)[len(self.stamps) // 2] if self.stamps else 0 return sorted(self.stamps[-101:])[50] def add_block(self): self.wall += self.rng.expovariate(1.0 / 600.0) floor = self.mtp11() + 1 stamp = min(self.wall + 7200, max(self.wall, floor)) self.stamps.append(stamp) return stamp def run_handoff(gate="mtp101", seed_parent=42, seed_fork=7, bias_blocks=50, seed_ema=True, parent_blocks=3000, fork_blocks=2000, parent_stamp_ahead=False): parent = ParentChain(seed_parent) for _ in range(parent_blocks): parent.add_block() t_act = parent.wall + 1200.0 while True: parent.add_block() mtp101 = parent.mtp101() last = parent.stamps[-1] gate_ok = {"mtp101": mtp101 >= t_act, "last": last >= t_act, "mtp-bias": mtp101 + bias_blocks * 600.0 >= t_act}[gate] if gate_ok: break fork_act_wall = parent.wall parent_last_stamp = parent.stamps[-1] if parent_stamp_ahead: parent_last_stamp = parent.wall + 7200 cfg = Cfg() chain = M.Chain(cfg) for s in parent.stamps[-101:]: bisect.insort(chain.mtp_sorted, s) chain.mtp_deque.append(s) chain.last_block_t = parent.wall chain.t = parent.wall chain.height = parent_blocks parent_mean_int = (parent.stamps[-1] - parent.stamps[-101]) / 100.0 if seed_ema: chain.avg_error = cfg.target - parent_mean_int chain.err_lp = chain.avg_error chain.err_lp_prev = chain.avg_error rng = random.Random(seed_fork) first_e = None first_wait_s = None for i in range(fork_blocks): dt = rng.expovariate(1.0 / (M.T * chain.d)) arr = chain.t + dt min_arr = chain.last_block_t + chain.delay if arr < min_arr: arr = min_arr if i == 0: # transition rule: fork block 1 stamps within +60s of now, # breaking monotonicity with the parent stamp if needed mtp_prev = chain.mtp() stamp = min(arr + cfg.future, max(arr, mtp_prev + 1)) if stamp < mtp_prev + 1: stamp = mtp_prev + 1 # the wait that FULL monotonicity would have imposed: full_floor = parent_last_stamp + 1 first_wait_s = max(0.0, full_floor - (arr + cfg.future)) # commit the first block by hand chain.advance_time(arr) chain.last_block_t = arr chain.push_stamp(stamp) e = max(1, stamp - parent_last_stamp) reward = M.R_FULL * e / M.T allowance = M.S_MAX * e / M.T size = min(allowance, chain.backlog) chain.backlog = max(0.0, chain.backlog - size) chain.max_block_size = max(chain.max_block_size, size) chain.issuance += reward chain.honest_reward += reward chain.height += 1 chain.blocks.append((arr, False, e, chain.d, size)) first_e = e continue chain.add_block(arr, False, mode="honest") ints = [b[2] for b in chain.blocks if b[2] > 0] warmup = ints[:100] warmup_mean = sum(warmup) / len(warmup) if warmup else 0 later = ints[200:] later_mean = sum(later) / len(later) if later else 0 parity = chain.issuance / (M.R_FULL * (chain.t - fork_act_wall) / M.T) if chain.t > fork_act_wall else 0 return { "gate": gate, "lag_h": (fork_act_wall - t_act) / 3600.0, "first_e": first_e, "first_wait_s": first_wait_s, "warmup_mean": warmup_mean, "later_mean": later_mean, "parity": parity, "d_final": chain.d, } def main(): print("activation handoff: gate variants") print(f"{'gate':10s} {'lag_h':>8s} {'warmup100':>10s} {'later_mean':>11s} " f"{'parity':>8s} {'d_fin':>7s} {'first_e':>8s}") for gate in ("mtp101", "last", "mtp-bias"): r = run_handoff(gate=gate) print(f"{gate:10s} {r['lag_h']:8.2f} {r['warmup_mean']:10.1f} " f"{r['later_mean']:11.1f} {r['parity']:8.4f} {r["d_final"]:7.3f} " f"{r['first_e']:8.1f}") if r["first_wait_s"] is not None: print(f"{'':10s} full-monotonic wait avoided: {r['first_wait_s']:,.0f}s") print("\nEMA seeding (gate mtp101):") print(f"{'seed_ema':>9s} {'warmup100':>10s} {'later_mean':>11s} {'parity':>8s}") for seed_ema in (True, False): r = run_handoff(gate="mtp101", seed_ema=seed_ema) print(f"{str(seed_ema):>9s} {r['warmup_mean']:10.1f} " f"{r['later_mean']:11.1f} {r['parity']:8.4f}") print("\nadversarial parent stamp (+2h ahead):") for gate in ("mtp101", "last"): r = run_handoff(gate=gate, parent_stamp_ahead=True) print(f"{gate:10s} lag_h={r['lag_h']:.2f} first_e={r['first_e']:.1f} " f"wait_avoided={r['first_wait_s']:,.0f}s warmup={r['warmup_mean']:.1f}s") if __name__ == "__main__": main()