From 44c8a5b7f427645c518919734a35d115742ce656 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Fri, 10 Apr 2026 10:36:59 +0200 Subject: [PATCH] eth: base protection quota on current peer count, not max capacity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- eth/dropper.go | 8 ++++---- eth/dropper_test.go | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/eth/dropper.go b/eth/dropper.go index 87c72cfa0f..2eabd92fe7 100644 --- a/eth/dropper.go +++ b/eth/dropper.go @@ -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 diff --git a/eth/dropper_test.go b/eth/dropper_test.go index 8c08893ee3..9f8b55a0b6 100644 --- a/eth/dropper_test.go +++ b/eth/dropper_test.go @@ -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)) } }