eth/txtracker: prevent disconnected peers from leaking back into stats

NotifyPeerDrop deleted t.peers[peer] but left t.txs entries pointing
to that peer. When those txs later finalized, checkFinalization
recreated the peer entry, and the EMA loop decayed it forever.

Fix: create peer entries in NotifyAccepted (when txs are first
accepted), not in handleChainHead or checkFinalization. Both chain
event handlers now skip peers with no entry — disconnected peers
whose entries were deleted by NotifyPeerDrop stay deleted.
This commit is contained in:
Csaba Kiraly 2026-04-11 16:31:44 +02:00
parent 7f1720b3dc
commit e2b620ab44
2 changed files with 14 additions and 11 deletions

View file

@ -131,6 +131,10 @@ func (t *Tracker) NotifyAccepted(peer string, hashes []common.Hash) {
t.txs[hash] = peer
t.order = append(t.order, hash)
}
// Ensure the delivering peer has a stats entry.
if len(hashes) > 0 && t.peers[peer] == nil {
t.peers[peer] = &peerStats{}
}
// Evict oldest entries if over capacity.
for len(t.txs) > maxTracked {
oldest := t.order[0]
@ -192,12 +196,9 @@ func (t *Tracker) handleChainHead(ev core.ChainHeadEvent) {
blockIncl[peer]++
}
}
// Ensure peers with inclusions in this block have entries.
for peer := range blockIncl {
if t.peers[peer] == nil {
t.peers[peer] = &peerStats{}
}
}
// Only credit peers that are still tracked (not disconnected).
// Don't create entries for unknown peers — they may have been
// removed by NotifyPeerDrop and should not be resurrected.
// Update EMA for all tracked peers (decay inactive ones).
for peer, ps := range t.peers {
ps.recentIncluded = (1-emaAlpha)*ps.recentIncluded + emaAlpha*float64(blockIncl[peer])
@ -231,8 +232,7 @@ func (t *Tracker) checkFinalization() {
}
ps := t.peers[peer]
if ps == nil {
ps = &peerStats{}
t.peers[peer] = ps
continue // peer disconnected, skip credit
}
ps.finalized++
credited++

View file

@ -110,10 +110,13 @@ func TestNotifyReceived(t *testing.T) {
txs := []*types.Transaction{makeTx(1), makeTx(2), makeTx(3)}
tr.NotifyAccepted("peerA", hashTxs(txs))
// No chain events yet — stats should be empty.
// No chain events yet — peer entry exists but with zero stats.
stats := tr.GetAllPeerStats()
if len(stats) != 0 {
t.Fatalf("expected empty stats before any chain events, got %d peers", len(stats))
if stats["peerA"].Finalized != 0 {
t.Fatalf("expected zero Finalized before chain events, got %d", stats["peerA"].Finalized)
}
if stats["peerA"].RecentIncluded != 0 {
t.Fatalf("expected zero RecentIncluded before chain events, got %f", stats["peerA"].RecentIncluded)
}
}