Three Classic Blockchain P2P Attacks, Rebuilt in a Rust Local Lab
Sybil, Eclipse, and Network Partition. From real incidents to local simulations
Three Classic Blockchain P2P Attacks, Rebuilt in a Rust Local Lab

Sybil, Eclipse, and Network Partition. From real incidents to local simulations
I wanted one local Rust lab where I could run three classic blockchain P2P failure modes with one CLI: Sybil, Eclipse, and network partition. The goal was not to attack a live network. The goal was to make each failure mode observable: which peers entered the table, which state message arrived, and whether the victim still had an honest peer to compare against.
Everything runs on *127.0.0.1* in rust-p2p-protocol-lab.
Overview: why these three attacks?
The three modes, in the order I ran them:
- Sybil: one actor creates many identities. Cheap wallets in airdrops, cheap
*NodeId*s in P2P; I track*sybil_peers / total_peers*to see how many peer slots one operator can fill. - Eclipse: those identities try to control what a victim sees. Real concern is attacker-controlled state; in the lab I check
*state_diverged*whether bad state sticks when honest peers remain. - Partition: two groups stop seeing each other. Real chain splits show why split views matter; locally I count
*cross_peers_remaining*to see whether Group A still advertises Group B.
Lab setup: one Rust workspace, three scenarios
Four crates, one runner:

Fig. 1. p2p-core → p2p-node / p2p-lab → p2p-env.
- `p2p-core
:NodeId,Message` ***p2p-node***: honest node behavior, peer table (*MAX_PEERS = 8*)***p2p-lab***: crawler and attack tools***p2p-env:** scenario runner (`cargo run -p p2p-env — …*`)

Fig. 2. Workspace build.
Protocol is small on purpose:

Fig. 3. Hello, Ping/Pong, GetPeers/Peers, Tip.
pub enum Message {
Hello {
node_id: NodeId,
listen_addr: SocketAddr,
peers: Vec<SocketAddr>,
},
Ping,
Pong,
GetPeers,
Peers(Vec<SocketAddr>),
Tip {
height: u64,
hash: String,
},
}
***Hello***: identity + listen address + known peers***Ping***/***Pong***: keepalive***GetPeers***/***Peers***: what the gym queries after each run***Tip***: a loggable state string so I can see which hash the victim picked up
*Tip* is not a validated block header. It exists so the Eclipse scenario has something to push besides peer-table entries.
Attack 1 — Sybil: many identities, limited peer slots
Real case
In May 2022, Optimism removed more than 17,000 Sybil addresses from OP Airdrop #1 — wallet farming that had slipped through initial filters. The Block reported the same enforcement. That is application-layer abuse, not a P2P attack. The contested resource was token eligibility, not peer slots.
The lab tests the same pressure with a different object: one operator, many cheap NodeIds, eight peer slots. Wallet farming and peer-table flooding are not the same mechanism. They share one question: how much can one actor gain by multiplying identities?
Local model
In my lab, Sybil identities are local nodes with different *NodeId*s. The victim has *MAX_PEERS = 8*. I launched 10 Sybil identities and measured how many peer slots they occupied.

Fig. 4. Eight slots; many identities compete to hold them open.
let occupancy = sybil_peers as f64 / peers.len().max(1) as f64 * 100.0;
cargo run -p p2p-env - - honest 4 - attack sybil - sybil 10

Fig. 5. 5/8 slots Sybil; occupancy: 62.5, Success: false.
I expected full capture when I outnumbered the table. I got 5 of 8 slots instead. Honest nodes connected during *reset()* before every Sybil session landed.
This was not Sybil success. It was 62.5% malicious occupancy, i.e., real pressure on the peer table, but three honest slots still held.
Attack 2 — Eclipse: fake state is not enough
Real case
Marcus et al.’s “False Friends” work showed an Ethereum node could be isolated by flooding its discovery table with attacker-controlled peers, then feeding that victim a filtered chain view. The attack targets peer selection and connection management — not consensus math. The paper reports that countermeasures were incorporated into Geth v1.9.0.
I am not reproducing that attack. It is the reference for why Eclipse sits next to Sybil: Sybil fills slots; Eclipse cuts honest cross-checks.
Local model
The lab strips that down to one check: attacker peers send a fake *Tip* — does the victim lose every honest peer?

Fig. 6. Eclipse path: connect, send fake Tip, check peer mix.
send_tip(&mut writer, Tip { height: 9999, hash: "attacker_tip_FAKE".into() }).await;
let honest_peers = peers.len().saturating_sub(sybil_peers);
let state_diverged = victim_tip != honest_tip && honest_peers == 0;
AttackResult { success: state_diverged, /* ... */ }
cargo run -p p2p-env - - honest 4 - attack eclipse - sybil 20

Fig. 7. attacker_tip_FAKE logged; 3/8 Sybil, 5 honest; state_diverged=false.
This was the most useful failed run. The fake state arrived, but the victim still had honest peers. I stopped treating Sybil and Eclipse as the same thing after this run. Sybil gave me peer pressure. Eclipse required removing the honest path.
In this lab, Eclipse requires *honest_peers == 0* before *state_diverged* can flip true.
Attack 3 — Network partition: when groups stop seeing each other
Real case
In August 2021, a Geth bug led some unpatched nodes onto a minority chain — a brief split until operators patched. Bitcoin had an earlier version of the same failure mode: BIP 50 documents a March 2013 fork where Bitcoin 0.8 accepted a block that pre-0.8 nodes rejected.
Neither incident used my blocklist model. Both show what happens when two groups stop sharing the same view: recovery becomes a client rollout and coordination problem, not just a consensus rule.
Partition is also the setup people often cite for double-spending scenarios: broadcast conflicting transactions to two isolated groups. That requires a transaction layer, which this lab does not have. Here I only check whether cross-group peers disappear.
Local model
Six local nodes, Group A *[0,1,2]* and Group B *[3,4,5]*, symmetric blocklists inside the process. Metric: does Group A’s peer table still list any Group B port?

Fig. 8. Symmetric blocklists; metric is cross_peers_remaining.
block_cross_group_links(env, &addrs_a, &addrs_b);
let cross_peers = a_peers
.iter()
.filter(|p| addrs_b.iter().any(|b| b.port() == p.port()))
.count();
AttackResult { success: cross_peers == 0, /* ... */ }```bash
cargo run -p p2p-env - - honest 6 - attack partition
![Fig. 9. Group A `[0,1,2]` / Group B `[3,4,5]`; `cross_peers_remaining: 0`, `Success: true`.](https://miro.medium.com/v2/resize:fit:1100/1*NvFlv1X6UcmGU_ygrhzczg.png)
Fig. 9. Group A [0,1,2] / Group B [3,4,5]; cross_peers_remaining: 0, Success: true.
This is an application-layer local simulation, not BGP routing and not a real Internet partition.
Closing
The most useful result was not the clean partition success. It was the failed Eclipse run. I could deliver fake state, but I could not make that fake state become the victim’s only view while honest peers remained. That gap is the real design space: peer replacement, anchor peers, diversity rules, identity cost, and monitoring.
Next knobs I want to turn in the same lab:
- signed
*NodeId*or another identity-cost mechanism - peer diversity policy (subnet / operator bucketing)
- anchor peers that eviction cannot drop
- different peer eviction rules under slot pressure
- IDS scoring on peer-table concentration
- rerun all three modes and compare
*occupancy*,*honest_peers*, and*cross_peers_remaining*
Appendix
Reproduce
git clone https://github.com/egpivo/rust-p2p-protocol-lab.git
cd rust-p2p-protocol-lab
cargo run -p p2p-env - - honest 4 - attack sybil - sybil 10
cargo run -p p2p-env - - honest 4 - attack eclipse - sybil 20
cargo run -p p2p-env - - honest 6 - attack partition
References
Sources cited for the real-world case introductions:
- Optimism, Let the Claims Begin (May 2022): OP Airdrop #1 launch; removal of 17,000+ Sybil addresses.
- The Block, Optimism Cracks Down on Airdrop Farmers (May 2022): press coverage of the same Sybil filtering.
- Marcus et al., Eclipsing Ethereum Peers with False Friends (arXiv, 2019): Ethereum eclipse attack via peer-table flooding; paper reports Geth v1.9.0 countermeasures.
- Go-Ethereum, Minority Split Postmortem (August 2021): August 2021 chain split affecting unpatched Geth nodes.
- BIP 50: March 2013 Chain Fork Post-Mortem: Bitcoin 0.8 vs pre-0.8 block acceptance mismatch.
This post was originally published on my personal blog: https://egpivo.github.io/2026/06/09/building-blockchain-p2p-security-gym-rust.html
메타데이터
- post_id
- fc13761904cc
- slug
- three-classic-blockchain-p2p-attacks-rebuilt-in-a-rust-local-lab-fc13761904cc
- url
- https://blog.stackademic.com/three-classic-blockchain-p2p-attacks-rebuilt-in-a-rust-local-lab-fc13761904cc
- canonical_url
- https://blog.stackademic.com/three-classic-blockchain-p2p-attacks-rebuilt-in-a-rust-local-lab-fc13761904cc
- author_url
- https://medium.com/@egpivo
- status
- ok
- fetched_at
- 2026-06-11 05:11:55