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.
This commit is contained in:
Csaba Kiraly 2026-04-10 10:36:59 +02:00
parent 6d53acfa22
commit 44c8a5b7f4
2 changed files with 14 additions and 9 deletions

View file

@ -218,8 +218,8 @@ func (cm *dropper) filterProtectedPeers(droppable []*p2p.Peer) []*p2p.Peer {
}
protectedSet := make(map[*p2p.Peer]struct{})
protectTopN := func(entries []peerWithStats, maxPeers int, cat protectionCategory) {
n := int(float64(maxPeers) * cat.frac)
protectTopN := func(entries []peerWithStats, cat protectionCategory) {
n := int(float64(len(entries)) * cat.frac)
if n == 0 || len(entries) == 0 {
return
}
@ -238,8 +238,8 @@ func (cm *dropper) filterProtectedPeers(droppable []*p2p.Peer) []*p2p.Peer {
dialCopy := make([]peerWithStats, len(dialed))
copy(dialCopy, dialed)
protectTopN(inCopy, cm.maxInboundPeers, cat)
protectTopN(dialCopy, cm.maxDialPeers, cat)
protectTopN(inCopy, cat)
protectTopN(dialCopy, cat)
}
if len(protectedSet) == 0 {
return droppable

View file

@ -43,7 +43,7 @@ func TestFilterProtectedEmptyStats(t *testing.T) {
}
func TestFilterProtectedTopPeer(t *testing.T) {
// 20 peers, maxDialPeers=20, 10% = 2 protected per category.
// 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}
@ -108,18 +108,23 @@ func TestFilterProtectedOverlap(t *testing.T) {
}
func TestFilterProtectedAllProtected(t *testing.T) {
// Only 2 droppable peers, both are top by different categories.
// 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(2)
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)
if len(result) != 0 {
t.Fatalf("expected all peers protected, got %d droppable", len(result))
// 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))
}
}