73 lines
3.1 KiB
Python
73 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Mission item 12, the eclipse bound of the weight-backed peer directory (docs/analysis/mission/invent.md 7.1, model E).
|
|
|
|
A fresh node draws its outbound peers from the directory WITHOUT replacement, each draw proportional to the listing
|
|
key's weight at the latest checkpoint (the fork's `FinalityManager::draw_peers`). An attacker holding share `a` of the
|
|
weight lists its addresses under `m` keys of equal weight (each above dust: the dust threshold is what bounds `m`, 100
|
|
blocks a window on mainnet). The node is eclipsed when every one of its `n` outbound peers is the attacker's.
|
|
|
|
Model E's closed form is a^n (with replacement); without replacement and with few attacker keys the odds are lower, and
|
|
with fewer than n attacker keys an eclipse is impossible. Honest keys: `h` keys with Zipf weights (a few large miners,
|
|
a long tail), the shape of a public chain's weight table.
|
|
|
|
python3 sim/peer-directory/eclipse.py [--starts 1000] [--share 0.34] [--peers 8,16] [--attacker-keys 8,64,1000]
|
|
[--honest-keys 200] [--seed 7]
|
|
"""
|
|
import argparse
|
|
import random
|
|
|
|
|
|
def draw(pool, n, rng):
|
|
"""Weighted draw without replacement: pool is a list of (is_attacker, weight)."""
|
|
pool = list(pool)
|
|
out = []
|
|
while len(out) < n and pool:
|
|
total = sum(w for _, w in pool)
|
|
r = rng.uniform(0, total)
|
|
i = 0
|
|
while i + 1 < len(pool) and r >= pool[i][1]:
|
|
r -= pool[i][1]
|
|
i += 1
|
|
out.append(pool.pop(i)[0])
|
|
return out
|
|
|
|
|
|
def run(starts, share, peers, attacker_keys, honest_keys, rng):
|
|
honest_total = 1.0 - share
|
|
zipf = [1.0 / (k + 1) for k in range(honest_keys)]
|
|
z = sum(zipf)
|
|
honest = [(False, honest_total * w / z) for w in zipf]
|
|
attacker = [(True, share / attacker_keys)] * attacker_keys
|
|
pool = honest + attacker
|
|
eclipsed = 0
|
|
majority = 0
|
|
for _ in range(starts):
|
|
got = draw(pool, peers, rng)
|
|
k = sum(got)
|
|
if k == len(got):
|
|
eclipsed += 1
|
|
if k * 2 > len(got):
|
|
majority += 1
|
|
return eclipsed, majority
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--starts", type=int, default=1000)
|
|
ap.add_argument("--share", type=float, default=0.34)
|
|
ap.add_argument("--peers", default="8,16")
|
|
ap.add_argument("--attacker-keys", default="8,64,1000")
|
|
ap.add_argument("--honest-keys", type=int, default=200)
|
|
ap.add_argument("--seed", type=int, default=7)
|
|
a = ap.parse_args()
|
|
rng = random.Random(a.seed)
|
|
print(f"starts {a.starts}, attacker share {a.share:.2f} of the weight, {a.honest_keys} honest keys (Zipf), seed {a.seed}")
|
|
print(f"{'peers':>5} {'attacker keys':>13} {'eclipsed':>9} {'rate':>10} {'a^n (model E)':>14} {'attacker majority':>17}")
|
|
for n in (int(x) for x in a.peers.split(",")):
|
|
for m in (int(x) for x in a.attacker_keys.split(",")):
|
|
e, maj = run(a.starts, a.share, n, m, a.honest_keys, rng)
|
|
print(f"{n:>5} {m:>13} {e:>9} {e / a.starts:>10.2e} {a.share ** n:>14.2e} {maj / a.starts:>17.3f}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|