go-ethereum/eth/dropper_test.go
Csaba Kiraly 44c8a5b7f4 eth: base protection quota on current peer count, not max capacity
protectTopN used maxPeers (configured capacity) to compute the
number of peers to protect. With small droppable sets this could
protect everyone, permanently disabling churn.

Use len(entries) (current droppable count in each category) instead.
With 20 droppable dialed peers and 10% fraction, 2 are protected.
With 3 droppable peers, 0 are protected — churn is never blocked.
2026-04-10 10:36:59 +02:00

130 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package eth
import (
"fmt"
"testing"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode"
)
func makePeers(n int) []*p2p.Peer {
peers := make([]*p2p.Peer, n)
for i := range peers {
id := enode.ID{byte(i)}
peers[i] = p2p.NewPeer(id, fmt.Sprintf("peer%d", i), nil)
}
return peers
}
func TestFilterProtectedNoStats(t *testing.T) {
// When the stats func returns nil/empty, all peers remain droppable.
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return nil }
peers := makePeers(10)
result := cm.filterProtectedPeers(peers)
if len(result) != len(peers) {
t.Fatalf("expected all peers droppable with nil stats, got %d/%d", len(result), len(peers))
}
}
func TestFilterProtectedEmptyStats(t *testing.T) {
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
cm.peerStatsFunc = func() map[string]PeerInclusionStats {
return map[string]PeerInclusionStats{}
}
peers := makePeers(10)
result := cm.filterProtectedPeers(peers)
if len(result) != len(peers) {
t.Fatalf("expected all peers droppable with empty stats, got %d/%d", len(result), len(peers))
}
}
func TestFilterProtectedTopPeer(t *testing.T) {
// 20 peers, 10% of 20 = 2 protected per category.
// NewPeer creates non-inbound peers, so all go to dialed bucket.
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
peers := makePeers(20)
stats := make(map[string]PeerInclusionStats)
// Peer 0: top by Finalized
stats[peers[0].ID().String()] = PeerInclusionStats{Finalized: 100}
// Peer 1: top by RecentIncluded
stats[peers[1].ID().String()] = PeerInclusionStats{RecentIncluded: 5.0}
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats }
result := cm.filterProtectedPeers(peers)
// 2 categories × 2 protected each = up to 4, but peers 0 and 1 are
// different so both should be removed. Other peers have zero scores.
protected := len(peers) - len(result)
if protected != 2 {
t.Fatalf("expected 2 protected peers, got %d", protected)
}
// Verify peers 0 and 1 are not in result.
for _, p := range result {
id := p.ID().String()
if id == peers[0].ID().String() || id == peers[1].ID().String() {
t.Fatalf("peer %s should be protected", id)
}
}
}
func TestFilterProtectedZeroScore(t *testing.T) {
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
peers := makePeers(10)
stats := make(map[string]PeerInclusionStats)
// All peers have zero stats.
for _, p := range peers {
stats[p.ID().String()] = PeerInclusionStats{}
}
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats }
result := cm.filterProtectedPeers(peers)
if len(result) != len(peers) {
t.Fatalf("expected no protection with zero scores, got %d protected", len(peers)-len(result))
}
}
func TestFilterProtectedOverlap(t *testing.T) {
// One peer is top in both categories — should only be removed once.
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
peers := makePeers(20)
stats := make(map[string]PeerInclusionStats)
// Peer 0 is top in both.
stats[peers[0].ID().String()] = PeerInclusionStats{Finalized: 100, RecentIncluded: 5.0}
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats }
result := cm.filterProtectedPeers(peers)
protected := len(peers) - len(result)
if protected != 1 {
t.Fatalf("expected 1 protected peer (overlap), got %d", protected)
}
}
func TestFilterProtectedAllProtected(t *testing.T) {
// 10 peers: 10% = 1 per category. Give all 10 peers high scores in
// one of the two categories so the union covers everyone.
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
peers := makePeers(10)
stats := make(map[string]PeerInclusionStats)
// Peer 0 has the highest Finalized → protected by total-finalized.
stats[peers[0].ID().String()] = PeerInclusionStats{Finalized: 100}
// Peer 1 has the highest RecentIncluded → protected by recent-included.
stats[peers[1].ID().String()] = PeerInclusionStats{RecentIncluded: 5.0}
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats }
result := cm.filterProtectedPeers(peers)
// 10% of 10 = 1 per category, 2 categories = 2 protected.
// 10 - 2 = 8 droppable.
if len(result) != 8 {
t.Fatalf("expected 8 droppable peers, got %d", len(result))
}
}