From bca1267b394667b2e98b161355489a7530125aa7 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Tue, 9 Jun 2026 14:48:36 +0300 Subject: [PATCH] fix(peermanager): prefer connected peers in selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter/lightpush peer selection (Automatic -> SelectRandom, and the service-slot pubsub path) chose uniformly at random among all peers that support the protocol+pubsub topic, with no regard for connectedness. A light node that has learned many peers via discv5/peer-exchange — some of them stale/unreachable (e.g. torn-down nodes whose ENRs a bootstrap node still advertised) — would therefore keep picking dead peers, churning on failed dials and missing messages, even while a perfectly good already- connected service node (e.g. the static fleet node) was available. Add preferConnectedPeers: bias selection toward peers we are currently connected to, falling back to the full candidate set when none are connected (so behavior is unchanged when there is no connected candidate). Relates to status-im/status-go#7513. Co-Authored-By: Claude Opus 4.8 (1M context) --- waku/v2/peermanager/peer_selection.go | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/waku/v2/peermanager/peer_selection.go b/waku/v2/peermanager/peer_selection.go index 007d1939..522cfc62 100644 --- a/waku/v2/peermanager/peer_selection.go +++ b/waku/v2/peermanager/peer_selection.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" + "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/protocol" "go.uber.org/zap" @@ -91,6 +92,10 @@ func (pm *PeerManager) SelectRandom(criteria PeerSelectionCriteria) (peer.IDSlic filteredPeers = pm.host.Peerstore().(wps.WakuPeerstore).PeersByPubSubTopics(criteria.PubsubTopics, filteredPeers...) } + // Prefer peers we are already connected to, so we don't churn on dialing + // stale/unreachable peers learned via discovery or peer-exchange. + filteredPeers = pm.preferConnectedPeers(filteredPeers) + //Not passing excludePeers as filterPeers are already considering excluded ones. randomPeers, err := selectRandomPeers(filteredPeers, nil, min(criteria.MaxPeers, len(peerIDs))) if err != nil && len(peerIDs) == 0 { @@ -121,6 +126,28 @@ func getRandom(filter PeerSet, count int, excludePeers PeerSet) (PeerSet, error) return selectedPeers, nil } +// preferConnectedPeers returns the subset of the given peers that we are +// currently connected to. If none are connected it returns the input unchanged. +// +// This biases peer selection toward reachable peers (e.g. an already-connected +// fleet/service node) instead of stale or unreachable peers learned via +// discv5/peer-exchange. Without this, selection is connectedness-blind and a +// light node can repeatedly pick dead peers for filter/lightpush, churning on +// failed dials and missing messages while a perfectly good connected peer is +// available (see status-im/status-go#7513). +func (pm *PeerManager) preferConnectedPeers(peers peer.IDSlice) peer.IDSlice { + connected := make(peer.IDSlice, 0, len(peers)) + for _, p := range peers { + if pm.host.Network().Connectedness(p) == network.Connected { + connected = append(connected, p) + } + } + if len(connected) > 0 { + return connected + } + return peers +} + // selects count random peers from list of peers func selectRandomPeers(peers peer.IDSlice, excludePeers PeerSet, count int) (PeerSet, error) { filteredPeerMap := PeerSliceToMap(peers) @@ -172,6 +199,8 @@ func (pm *PeerManager) selectServicePeer(criteria PeerSelectionCriteria) (PeerSe } slot.mu.RUnlock() selectedPeers := pm.host.Peerstore().(wps.WakuPeerstore).PeersByPubSubTopics(criteria.PubsubTopics, keys...) + // Prefer already-connected service peers over stale/unreachable ones. + selectedPeers = pm.preferConnectedPeers(selectedPeers) tmpPeers, err := selectRandomPeers(selectedPeers, criteria.ExcludePeers, criteria.MaxPeers) for tmpPeer := range tmpPeers { peers[tmpPeer] = struct{}{}