eth: drop PeerInclusionStats wrapper and use txtracker.PeerStats directly

PeerInclusionStats was declared identically to txtracker.PeerStats as a
decoupling abstraction: any stats provider could implement the dropper's
callback by returning this shape. In practice there's one provider and
the two types were kept in sync by a rote copy adapter in backend.go.

Delete PeerInclusionStats, have the dropper consume txtracker.PeerStats
directly via getPeerStatsFunc. backend.go now passes
txTracker.GetAllPeerStats as the callback with no adapter.

If a second stats provider ever appears, the abstraction can come back;
until then, one fewer type and 8 fewer lines of ceremony.
This commit is contained in:
Csaba Kiraly 2026-04-15 14:35:37 +02:00
parent 69a7baefd8
commit 1f2ebc5d59
3 changed files with 33 additions and 46 deletions

View file

@ -463,14 +463,7 @@ func (s *Ethereum) Start() error {
s.handler.txTracker.Start(s.blockchain) s.handler.txTracker.Start(s.blockchain)
// Start the connection manager with inclusion-based peer protection. // Start the connection manager with inclusion-based peer protection.
s.dropper.Start(s.p2pServer, func() bool { return !s.Synced() }, func() map[string]PeerInclusionStats { s.dropper.Start(s.p2pServer, func() bool { return !s.Synced() }, s.handler.txTracker.GetAllPeerStats)
stats := s.handler.txTracker.GetAllPeerStats()
result := make(map[string]PeerInclusionStats, len(stats))
for id, ps := range stats {
result[id] = PeerInclusionStats{Finalized: ps.Finalized, RecentIncluded: ps.RecentIncluded}
}
return result
})
// start log indexer // start log indexer
s.filterMaps.Start() s.filterMaps.Start()

View file

@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/eth/txtracker"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
@ -58,30 +59,22 @@ var (
dropSkipped = metrics.NewRegisteredMeter("eth/dropper/skipped", nil) dropSkipped = metrics.NewRegisteredMeter("eth/dropper/skipped", nil)
) )
// PeerInclusionStats holds the per-peer inclusion data needed by the dropper
// to decide which peers to protect. Any stats provider (e.g. txtracker) can
// implement getPeerInclusionStatsFunc by returning this struct per peer ID.
type PeerInclusionStats struct {
Finalized int64 // Cumulative finalized inclusions attributed to this peer
RecentIncluded float64 // EMA of per-block inclusions (0 if not tracked)
}
// Callback type to get per-peer inclusion statistics. // Callback type to get per-peer inclusion statistics.
type getPeerInclusionStatsFunc func() map[string]PeerInclusionStats type getPeerStatsFunc func() map[string]txtracker.PeerStats
// protectionCategory defines a peer scoring function and the fraction of peers // protectionCategory defines a peer scoring function and the fraction of peers
// to protect per inbound/dialed category. Multiple categories are unioned. // to protect per inbound/dialed category. Multiple categories are unioned.
type protectionCategory struct { type protectionCategory struct {
name string name string
score func(PeerInclusionStats) float64 score func(txtracker.PeerStats) float64
frac float64 // fraction of max peers to protect (0.01.0) frac float64 // fraction of max peers to protect (0.01.0)
} }
// protectionCategories is the list of protection criteria. Each category // protectionCategories is the list of protection criteria. Each category
// independently selects its top-N peers per pool; the union is protected. // independently selects its top-N peers per pool; the union is protected.
var protectionCategories = []protectionCategory{ var protectionCategories = []protectionCategory{
{"total-finalized", func(s PeerInclusionStats) float64 { return float64(s.Finalized) }, inclusionProtectionFrac}, {"total-finalized", func(s txtracker.PeerStats) float64 { return float64(s.Finalized) }, inclusionProtectionFrac},
{"recent-included", func(s PeerInclusionStats) float64 { return s.RecentIncluded }, inclusionProtectionFrac}, {"recent-included", func(s txtracker.PeerStats) float64 { return s.RecentIncluded }, inclusionProtectionFrac},
} }
// dropper monitors the state of the peer pool and introduces churn by // dropper monitors the state of the peer pool and introduces churn by
@ -107,7 +100,7 @@ type dropper struct {
maxInboundPeers int // maximum number of inbound peers maxInboundPeers int // maximum number of inbound peers
peersFunc getPeersFunc peersFunc getPeersFunc
syncingFunc getSyncingFunc syncingFunc getSyncingFunc
peerStatsFunc getPeerInclusionStatsFunc // optional: inclusion stats for protection peerStatsFunc getPeerStatsFunc // optional: inclusion stats for protection
// peerDropTimer introduces churn if we are close to limit capacity. // peerDropTimer introduces churn if we are close to limit capacity.
// We handle Dialed and Inbound connections separately // We handle Dialed and Inbound connections separately
@ -139,7 +132,7 @@ func newDropper(maxDialPeers, maxInboundPeers int) *dropper {
// Start the dropper. peerStatsFunc is optional (nil disables inclusion // Start the dropper. peerStatsFunc is optional (nil disables inclusion
// protection). // protection).
func (cm *dropper) Start(srv *p2p.Server, syncingFunc getSyncingFunc, peerStatsFunc getPeerInclusionStatsFunc) { func (cm *dropper) Start(srv *p2p.Server, syncingFunc getSyncingFunc, peerStatsFunc getPeerStatsFunc) {
cm.peersFunc = srv.Peers cm.peersFunc = srv.Peers
cm.syncingFunc = syncingFunc cm.syncingFunc = syncingFunc
cm.peerStatsFunc = peerStatsFunc cm.peerStatsFunc = peerStatsFunc
@ -234,7 +227,7 @@ func (cm *dropper) protectedPeers(peers []*p2p.Peer) map[*p2p.Peer]bool {
// Factored from protectedPeers so tests can exercise the per-pool // Factored from protectedPeers so tests can exercise the per-pool
// selection logic without needing to construct direction-flagged // selection logic without needing to construct direction-flagged
// *p2p.Peer instances (which require unexported p2p types). // *p2p.Peer instances (which require unexported p2p types).
func protectedPeersByPool(inbound, dialed []*p2p.Peer, stats map[string]PeerInclusionStats) map[*p2p.Peer]bool { func protectedPeersByPool(inbound, dialed []*p2p.Peer, stats map[string]txtracker.PeerStats) map[*p2p.Peer]bool {
result := make(map[*p2p.Peer]bool) result := make(map[*p2p.Peer]bool)
// protectPool selects the top-frac peers from pool by score and adds them to result. // protectPool selects the top-frac peers from pool by score and adds them to result.
protectPool := func(pool []*p2p.Peer, score func(*p2p.Peer) float64, frac float64) { protectPool := func(pool []*p2p.Peer, score func(*p2p.Peer) float64, frac float64) {

View file

@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/ethereum/go-ethereum/eth/txtracker"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
) )
@ -35,7 +36,7 @@ func makePeers(n int) []*p2p.Peer {
func TestProtectedPeersNoStats(t *testing.T) { func TestProtectedPeersNoStats(t *testing.T) {
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return nil } cm.peerStatsFunc = func() map[string]txtracker.PeerStats { return nil }
peers := makePeers(10) peers := makePeers(10)
protected := cm.protectedPeers(peers) protected := cm.protectedPeers(peers)
@ -46,8 +47,8 @@ func TestProtectedPeersNoStats(t *testing.T) {
func TestProtectedPeersEmptyStats(t *testing.T) { func TestProtectedPeersEmptyStats(t *testing.T) {
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
cm.peerStatsFunc = func() map[string]PeerInclusionStats { cm.peerStatsFunc = func() map[string]txtracker.PeerStats {
return map[string]PeerInclusionStats{} return map[string]txtracker.PeerStats{}
} }
peers := makePeers(10) peers := makePeers(10)
@ -62,11 +63,11 @@ func TestProtectedPeersTopPeer(t *testing.T) {
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
peers := makePeers(20) peers := makePeers(20)
stats := make(map[string]PeerInclusionStats) stats := make(map[string]txtracker.PeerStats)
stats[peers[0].ID().String()] = PeerInclusionStats{Finalized: 100} stats[peers[0].ID().String()] = txtracker.PeerStats{Finalized: 100}
stats[peers[1].ID().String()] = PeerInclusionStats{RecentIncluded: 5.0} stats[peers[1].ID().String()] = txtracker.PeerStats{RecentIncluded: 5.0}
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats } cm.peerStatsFunc = func() map[string]txtracker.PeerStats { return stats }
protected := cm.protectedPeers(peers) protected := cm.protectedPeers(peers)
if len(protected) != 2 { if len(protected) != 2 {
@ -84,11 +85,11 @@ func TestProtectedPeersZeroScore(t *testing.T) {
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
peers := makePeers(10) peers := makePeers(10)
stats := make(map[string]PeerInclusionStats) stats := make(map[string]txtracker.PeerStats)
for _, p := range peers { for _, p := range peers {
stats[p.ID().String()] = PeerInclusionStats{} stats[p.ID().String()] = txtracker.PeerStats{}
} }
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats } cm.peerStatsFunc = func() map[string]txtracker.PeerStats { return stats }
protected := cm.protectedPeers(peers) protected := cm.protectedPeers(peers)
if len(protected) != 0 { if len(protected) != 0 {
@ -101,10 +102,10 @@ func TestProtectedPeersOverlap(t *testing.T) {
cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30} cm := &dropper{maxDialPeers: 20, maxInboundPeers: 30}
peers := makePeers(20) peers := makePeers(20)
stats := make(map[string]PeerInclusionStats) stats := make(map[string]txtracker.PeerStats)
stats[peers[0].ID().String()] = PeerInclusionStats{Finalized: 100, RecentIncluded: 5.0} stats[peers[0].ID().String()] = txtracker.PeerStats{Finalized: 100, RecentIncluded: 5.0}
cm.peerStatsFunc = func() map[string]PeerInclusionStats { return stats } cm.peerStatsFunc = func() map[string]txtracker.PeerStats { return stats }
protected := cm.protectedPeers(peers) protected := cm.protectedPeers(peers)
if len(protected) != 1 { if len(protected) != 1 {
@ -137,12 +138,12 @@ func TestProtectedByPoolPerPoolTopN(t *testing.T) {
dialed[i] = p2p.NewPeer(id, fmt.Sprintf("dialed%d", i), nil) dialed[i] = p2p.NewPeer(id, fmt.Sprintf("dialed%d", i), nil)
} }
// Strictly increasing scores: highest wins in each pool. // Strictly increasing scores: highest wins in each pool.
stats := make(map[string]PeerInclusionStats) stats := make(map[string]txtracker.PeerStats)
for i, p := range inbound { for i, p := range inbound {
stats[p.ID().String()] = PeerInclusionStats{Finalized: int64(1 + i)} stats[p.ID().String()] = txtracker.PeerStats{Finalized: int64(1 + i)}
} }
for i, p := range dialed { for i, p := range dialed {
stats[p.ID().String()] = PeerInclusionStats{Finalized: int64(1 + i)} stats[p.ID().String()] = txtracker.PeerStats{Finalized: int64(1 + i)}
} }
protected := protectedPeersByPool(inbound, dialed, stats) protected := protectedPeersByPool(inbound, dialed, stats)
@ -172,10 +173,10 @@ func TestProtectedByPoolCrossCategoryOverlap(t *testing.T) {
// Finalized winners: P2 (tie-broken-ok), P0 // Finalized winners: P2 (tie-broken-ok), P0
// RecentIncluded winners: P2, P1 // RecentIncluded winners: P2, P1
// Union: {P0, P1, P2}. // Union: {P0, P1, P2}.
stats := make(map[string]PeerInclusionStats) stats := make(map[string]txtracker.PeerStats)
stats[dialed[0].ID().String()] = PeerInclusionStats{Finalized: 100, RecentIncluded: 0} stats[dialed[0].ID().String()] = txtracker.PeerStats{Finalized: 100, RecentIncluded: 0}
stats[dialed[1].ID().String()] = PeerInclusionStats{Finalized: 0, RecentIncluded: 5.0} stats[dialed[1].ID().String()] = txtracker.PeerStats{Finalized: 0, RecentIncluded: 5.0}
stats[dialed[2].ID().String()] = PeerInclusionStats{Finalized: 200, RecentIncluded: 10.0} stats[dialed[2].ID().String()] = txtracker.PeerStats{Finalized: 200, RecentIncluded: 10.0}
protected := protectedPeersByPool(nil, dialed, stats) protected := protectedPeersByPool(nil, dialed, stats)
@ -202,13 +203,13 @@ func TestProtectedByPoolPerPoolIndependence(t *testing.T) {
id := enode.ID{byte(100 + i)} id := enode.ID{byte(100 + i)}
dialed[i] = p2p.NewPeer(id, fmt.Sprintf("dialed%d", i), nil) dialed[i] = p2p.NewPeer(id, fmt.Sprintf("dialed%d", i), nil)
} }
stats := make(map[string]PeerInclusionStats) stats := make(map[string]txtracker.PeerStats)
// Every inbound peer outscores every dialed peer. // Every inbound peer outscores every dialed peer.
for i, p := range inbound { for i, p := range inbound {
stats[p.ID().String()] = PeerInclusionStats{Finalized: int64(1000 + i)} stats[p.ID().String()] = txtracker.PeerStats{Finalized: int64(1000 + i)}
} }
for i, p := range dialed { for i, p := range dialed {
stats[p.ID().String()] = PeerInclusionStats{Finalized: int64(1 + i)} stats[p.ID().String()] = txtracker.PeerStats{Finalized: int64(1 + i)}
} }
protected := protectedPeersByPool(inbound, dialed, stats) protected := protectedPeersByPool(inbound, dialed, stats)