#!/usr/bin/env python3 """ Reorg race simulation - private sidechain vs public chain. Two chains race from a common tip: honest : PoW rate 1/600s, PI-controlled delay D(e) attacker : PoW rate X/600s, delay floor D*hw per block Key properties: - minority attackers (X < 1) never outrun - majority attackers outrun but speed capped at 1/D - reorg depth quantized by delay: k confirmations costs k*D wall time - asymmetric PI: fast difficulty drop (ki_down=50M), slow rise (ki_up=5M) Run: python3 reorg_sim.py """ import argparse import random import sys T = 600.0 # target cadence D_MIN = 60.0 # delay floor KP = 10e6 # proportional gain KI_UP = 5e6 # integral gain (difficulty drop - fast) KI_DOWN = 50e6 # integral gain (difficulty rise - slow) ALPHA = 0.05 # EMA smoothing SETPOINT = 617.0 # target interval (setpoint shift) def delay_from_e(e): """Compute delay from error e (seconds deviation from target).""" # PI controller: delay = kp * e + ki * integral # For simplicity, use proportional only for the delay floor # The actual delay is max(D_MIN, kp * abs(e) / 1e9) delay = D_MIN + max(0, KP * abs(e) / 1e9) return delay def race(X, hw, days, seed=7): """Race the honest chain against a private attacker chain.""" rng = random.Random(seed) t = 0.0 honest = 0 attacker = 0 prev_honest_t = 0.0 prev_att_t = 0.0 honest_e = 0.0 # error accumulator for honest chain horizon = days * 86400.0 outrun = None while t < horizon: # Honest chain delay dt_h = rng.expovariate(1.0 / T) h_delay = delay_from_e(honest_e) h_arr = t + max(dt_h, (prev_honest_t + h_delay) - t) if honest > 0 else t + dt_h # Attacker chain delay (constant hw multiplier) dt_a = rng.expovariate(X / T) a_delay = D_MIN * hw a_arr = t + max(dt_a, (prev_att_t + a_delay) - t) if attacker > 0 else t + dt_a # Process next block if h_arr <= a_arr: if honest == 0 or h_arr <= t: h_arr = t + dt_h t = h_arr honest += 1 prev_honest_t = t # Update error (actual interval vs target) if honest > 1: actual_interval = t - (prev_honest_t - h_delay) honest_e = honest_e * (1 - ALPHA) + (actual_interval - SETPOINT) * ALPHA else: honest_e = 0.0 else: if attacker == 0 or a_arr <= t: a_arr = t + dt_a t = a_arr attacker += 1 prev_att_t = t if outrun is None and attacker >= honest and honest > 0 and attacker > 0: outrun = t / 3600.0 return honest, attacker, outrun def main(): ap = argparse.ArgumentParser() ap.add_argument("--days", type=float, default=30.0) ap.add_argument("--seed", type=int, default=7) args = ap.parse_args() print(f"reorg race, {args.days} days, D_MIN={D_MIN}s, KP={KP/1e6}M, KI_UP={KI_UP/1e6}M, KI_DOWN={KI_DOWN/1e6}M") print(f"{'X':>7s} {'hw':>4s} | {'honest':>7s} {'attacker':>9s} {'ratio':>7s} | {'outrun_h':>9s}") for X in (0.5, 1.0, 2.0, 10.0, 100.0, 1e6): for hw in (1.0, 0.5): honest, attacker, outrun = race(X, hw, args.days, args.seed) ratio = attacker / max(1, honest) out_s = f"{outrun:8.1f}h" if outrun is not None else " never" print(f"{X:7.1f} {hw:4.1f} | {honest:7d} {attacker:9d} {ratio:7.2f} | {out_s}") print() print("catch-up cost: blocks to reorg k honest confirmations (D_MIN=60s each)") for k in (6, 144, 2016): print(f" {k} confirmations: minimum {k * D_MIN / 3600:.1f}h of attacker wall time") if __name__ == "__main__": main()