|
proc prunePeerStore*(pm: PeerManager) = |
|
let peerStore = pm.switch.peerStore |
|
let numPeers = peerStore[AddressBook].book.len |
|
let capacity = peerStore.getCapacity() |
|
if numPeers <= capacity: |
|
return |
|
|
|
trace "Peer store capacity exceeded", numPeers = numPeers, capacity = capacity |
|
let pruningCount = numPeers - capacity |
|
var peersToPrune: HashSet[PeerId] |
|
|
|
# prune failed connections |
|
for peerId, count in peerStore[NumberFailedConnBook].book.pairs: |
|
if count < pm.maxFailedAttempts: |
|
continue |
|
|
|
if peersToPrune.len >= pruningCount: |
|
break |
|
|
|
peersToPrune.incl(peerId) |
|
|
|
var notConnected = peerStore.getDisconnectedPeers().mapIt(it.peerId) |
|
|
|
# Always pick random non-connected peers |
|
shuffle(notConnected) |
|
|
|
var shardlessPeers: seq[PeerId] |
|
var peersByShard = initTable[uint16, seq[PeerId]]() |
|
|
|
for peer in notConnected: |
|
if not peerStore[ENRBook].contains(peer): |
|
shardlessPeers.add(peer) |
|
continue |
|
|
|
let record = peerStore[ENRBook][peer] |
|
|
|
let rec = record.toTyped().valueOr: |
|
shardlessPeers.add(peer) |
|
continue |
|
|
|
let rs = rec.relaySharding().valueOr: |
|
shardlessPeers.add(peer) |
|
continue |
|
|
|
for shard in rs.shardIds: |
|
peersByShard.mgetOrPut(shard, @[]).add(peer) |
|
|
|
# prune not connected peers without shard |
|
for peer in shardlessPeers: |
|
if peersToPrune.len >= pruningCount: |
|
break |
|
|
|
peersToPrune.incl(peer) |
|
|
|
# calculate the avg peers per shard |
|
let total = sum(toSeq(peersByShard.values).mapIt(it.len)) |
|
let avg = min(1, total div max(1, peersByShard.len)) |
|
|
|
# prune peers from shard with higher than avg count |
|
for shard, peers in peersByShard.pairs: |
|
let count = max(peers.len - avg, 0) |
|
for peer in peers[0 .. count]: |
|
if peersToPrune.len >= pruningCount: |
|
break |
|
|
|
peersToPrune.incl(peer) |
|
|
|
for peer in peersToPrune: |
|
peerStore.delete(peer) |
|
|
|
let afterNumPeers = peerStore[AddressBook].book.len |
|
|
|
trace "Finished pruning peer store", |
|
beforeNumPeers = numPeers, |
|
afterNumPeers = afterNumPeers, |
|
capacity = capacity, |
|
pruned = peersToPrune.len |
Summary
Peer-exchange responds with ENRs read from the PeerStore
ENRBook, which is fed add-only from discv5 and is only ever pruned on capacity, never on staleness/liveness. As a result a node keeps advertising ENRs of disconnected/dead discv5 peers via PX more or less indefinitely (as long as the store is under capacity). PX clients (e.g. go-waku light clients) then repeatedly try to dial unreachable peers.Permalinks below pinned to
master@86e424c(nodevelopbranch exists).The chain
PX serves from the PeerStore, and its only liveness gate is
CannotConnect.getEnrsFromStoreiteratespeerStore[ENRBook], skips onlyCannotConnect, andpoolFilterthen keeps onlyorigin == Discv5:logos-delivery/waku/waku_peer_exchange/protocol.nim
Lines 96 to 122 in 86e424c
logos-delivery/waku/waku_peer_exchange/protocol.nim
Lines 79 to 88 in 86e424c
The
ENRBookis populated add-only by the discv5 search loop (addPeer(peer, PeerOrigin.Discv5)), with no reverse path that removes an entry when discv5 later evicts it:logos-delivery/waku/discovery/waku_discv5.nim
Lines 252 to 290 in 86e424c
The only removal path is capacity-based, not staleness-based.
prunePeerStorereturns early unlessnumPeers > capacity, runs every 10 min, and when it does fire it evicts by failed-conn-count / shard balancing — never by "this ENR is stale":logos-delivery/waku/node/peer_manager/peer_manager.nim
Lines 1014 to 1090 in 86e424c
logos-delivery/waku/node/peer_manager/peer_manager.nim
Line 78 in 86e424c
Why the existing
CannotConnectfilter doesn't catch itA discv5-discovered or recently-disconnected peer sits at
NotConnected; it only becomesCannotConnectif this node actively dials it and fails — which doesn't happen for peers it never tries to dial. discv5's own routing-table revalidation does drop dead nodes quickly (~seconds), but that eviction is not propagated to the PeerStore that PX reads. #3652 (enrCache removal) reduced this but did not close it.Impact
Amplified in CI / ephemeral-node fleets (random keys + constant teardown on a shared bootnode). Concretely it breaks status-go functional light-client tests, where light clients fetch dead full-node ENRs via PX and time out on filter/lightpush: status-im/status-go#7513.
Suggested directions
ENRBook(or propagate discv5 evictions into the PeerStore); orNotConnectedingetEnrsFromStore— but carefully, since discv5 peers are legitimatelyNotConnected.