reorg_sim.py raw
1 #!/usr/bin/env python3
2 """
3 Reorg race simulation - private sidechain vs public chain.
4
5 Two chains race from a common tip:
6 honest : PoW rate 1/600s, PI-controlled delay D(e)
7 attacker : PoW rate X/600s, delay floor D*hw per block
8
9 Key properties:
10 - minority attackers (X < 1) never outrun
11 - majority attackers outrun but speed capped at 1/D
12 - reorg depth quantized by delay: k confirmations costs k*D wall time
13 - asymmetric PI: fast difficulty drop (ki_down=50M), slow rise (ki_up=5M)
14
15 Run: python3 reorg_sim.py
16 """
17 import argparse
18 import random
19 import sys
20
21 T = 600.0 # target cadence
22 D_MIN = 60.0 # delay floor
23 KP = 10e6 # proportional gain
24 KI_UP = 5e6 # integral gain (difficulty drop - fast)
25 KI_DOWN = 50e6 # integral gain (difficulty rise - slow)
26 ALPHA = 0.05 # EMA smoothing
27 SETPOINT = 617.0 # target interval (setpoint shift)
28
29
30 def delay_from_e(e):
31 """Compute delay from error e (seconds deviation from target)."""
32 # PI controller: delay = kp * e + ki * integral
33 # For simplicity, use proportional only for the delay floor
34 # The actual delay is max(D_MIN, kp * abs(e) / 1e9)
35 delay = D_MIN + max(0, KP * abs(e) / 1e9)
36 return delay
37
38
39 def race(X, hw, days, seed=7):
40 """Race the honest chain against a private attacker chain."""
41 rng = random.Random(seed)
42 t = 0.0
43 honest = 0
44 attacker = 0
45 prev_honest_t = 0.0
46 prev_att_t = 0.0
47 honest_e = 0.0 # error accumulator for honest chain
48 horizon = days * 86400.0
49 outrun = None
50
51 while t < horizon:
52 # Honest chain delay
53 dt_h = rng.expovariate(1.0 / T)
54 h_delay = delay_from_e(honest_e)
55 h_arr = t + max(dt_h, (prev_honest_t + h_delay) - t) if honest > 0 else t + dt_h
56
57 # Attacker chain delay (constant hw multiplier)
58 dt_a = rng.expovariate(X / T)
59 a_delay = D_MIN * hw
60 a_arr = t + max(dt_a, (prev_att_t + a_delay) - t) if attacker > 0 else t + dt_a
61
62 # Process next block
63 if h_arr <= a_arr:
64 if honest == 0 or h_arr <= t:
65 h_arr = t + dt_h
66 t = h_arr
67 honest += 1
68 prev_honest_t = t
69 # Update error (actual interval vs target)
70 if honest > 1:
71 actual_interval = t - (prev_honest_t - h_delay)
72 honest_e = honest_e * (1 - ALPHA) + (actual_interval - SETPOINT) * ALPHA
73 else:
74 honest_e = 0.0
75 else:
76 if attacker == 0 or a_arr <= t:
77 a_arr = t + dt_a
78 t = a_arr
79 attacker += 1
80 prev_att_t = t
81
82 if outrun is None and attacker >= honest and honest > 0 and attacker > 0:
83 outrun = t / 3600.0
84
85 return honest, attacker, outrun
86
87
88 def main():
89 ap = argparse.ArgumentParser()
90 ap.add_argument("--days", type=float, default=30.0)
91 ap.add_argument("--seed", type=int, default=7)
92 args = ap.parse_args()
93
94 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")
95 print(f"{'X':>7s} {'hw':>4s} | {'honest':>7s} {'attacker':>9s} {'ratio':>7s} | {'outrun_h':>9s}")
96 for X in (0.5, 1.0, 2.0, 10.0, 100.0, 1e6):
97 for hw in (1.0, 0.5):
98 honest, attacker, outrun = race(X, hw, args.days, args.seed)
99 ratio = attacker / max(1, honest)
100 out_s = f"{outrun:8.1f}h" if outrun is not None else " never"
101 print(f"{X:7.1f} {hw:4.1f} | {honest:7d} {attacker:9d} {ratio:7.2f} | {out_s}")
102
103 print()
104 print("catch-up cost: blocks to reorg k honest confirmations (D_MIN=60s each)")
105 for k in (6, 144, 2016):
106 print(f" {k} confirmations: minimum {k * D_MIN / 3600:.1f}h of attacker wall time")
107
108
109 if __name__ == "__main__":
110 main()
111