Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Eclipse attacks on Bitcoin’s peer table (Heilman et al. 2015)#
A Bitcoin node chooses its outbound connections from a table of addresses it has heard about. Heilman, Kendler, Zohar and Goldberg showed that an attacker who floods that table with its own addresses, and waits for a restart, can own every connection. The eclipsed node then sees only what the attacker shows it: it can be fed a fake chain or have its blocks withheld. Bitcoin’s defense is bucketing, which the paper’s countermeasures strengthened: an address’s bucket depends on its network group, and each group can reach only a few buckets.
What to look for#
Without bucketing, a flood of 5000 attacker addresses from four network groups fills nearly the whole table, and most sets of eight connections are entirely the attacker’s. With bucketing, the same flood is confined to at most 16 of the 64 buckets, and choosing a bucket before an entry makes an all-attacker selection vanishingly rare.
The history behind this experiment: Breakthroughs in Peer-to-Peer Networking. See Exercises: networking for a worked solution to the exercise.
Flood a table with and without bucketing#
import matplotlib.pyplot as plt
import blockchainkit as bk
results = {}
for label, per_group in (("unbucketed", None), ("bucketed", 4)):
table = bk.network.AddressManager(
buckets=64, bucket_size=16, buckets_per_group=per_group, seed=1
)
for i in range(1000): # Honest addresses, each from its own group.
table.add(f"honest-{i}", f"group-{i}")
for i in range(5000): # The attacker controls four groups.
table.add(f"attacker-{i}", f"attacker-group-{i % 4}")
share = sum(a.startswith("attacker") for a in table.addresses) / len(table.addresses)
eclipsed = (
sum(all(a.startswith("attacker") for a in table.select(8)) for _ in range(2000)) / 2000
)
buckets = {table.bucket_of(f"attacker-{i}", f"attacker-group-{i % 4}") for i in range(5000)}
results[label] = (share, eclipsed)
print(f"{label}: attacker share {share:.2f} in {len(buckets)} buckets, eclipsed {eclipsed:.4f}")
assert results["unbucketed"][1] > 0.5
assert len(buckets) <= 4 * 4 and results["bucketed"][1] == 0
unbucketed: attacker share 0.99 in 64 buckets, eclipsed 0.9210
bucketed: attacker share 0.27 in 16 buckets, eclipsed 0.0000
Exercise#
In the bucketed table, a selection picks a random nonempty bucket first. If the attacker owns 16 of 64 buckets outright, what is the probability that all eight picks land in its buckets? Compare with the simulation.
Total running time of the script: (0 minutes 0.502 seconds)
