@@ -30,6 +30,13 @@ import (
3030const (
3131 defaultEvaluationInterval = 10 * time .Second
3232 defaultStaleTipThreshold = 60 * time .Second
33+
34+ // DefaultMaxTrackedPeers is the maximum number of peers tracked by
35+ // the ChainSelector. When a new peer is added and the limit is reached,
36+ // the least-recently-updated peer is evicted. This bounds memory usage
37+ // and CPU cost of chain selection, preventing Sybil-based resource
38+ // exhaustion.
39+ DefaultMaxTrackedPeers = 200
3340)
3441
3542// safeBlockDiff computes the difference between two block numbers as int64,
@@ -64,19 +71,21 @@ type ChainSelectorConfig struct {
6471 EvaluationInterval time.Duration
6572 StaleTipThreshold time.Duration
6673 SecurityParam uint64
74+ MaxTrackedPeers int // 0 means use DefaultMaxTrackedPeers
6775}
6876
6977// ChainSelector tracks chain tips from multiple peers and selects the best
7078// chain according to Ouroboros Praos rules.
7179type ChainSelector struct {
72- config ChainSelectorConfig
73- securityParam uint64
74- peerTips map [ouroboros.ConnectionId ]* PeerChainTip
75- bestPeerConn * ouroboros.ConnectionId
76- localTip ochainsync.Tip
77- mutex sync.RWMutex
78- ctx context.Context
79- cancel context.CancelFunc
80+ config ChainSelectorConfig
81+ securityParam uint64
82+ maxTrackedPeers int
83+ peerTips map [ouroboros.ConnectionId ]* PeerChainTip
84+ bestPeerConn * ouroboros.ConnectionId
85+ localTip ochainsync.Tip
86+ mutex sync.RWMutex
87+ ctx context.Context
88+ cancel context.CancelFunc
8089}
8190
8291// NewChainSelector creates a new ChainSelector with the given configuration.
@@ -91,10 +100,15 @@ func NewChainSelector(cfg ChainSelectorConfig) *ChainSelector {
91100 if cfg .StaleTipThreshold == 0 {
92101 cfg .StaleTipThreshold = defaultStaleTipThreshold
93102 }
103+ maxPeers := cfg .MaxTrackedPeers
104+ if maxPeers <= 0 {
105+ maxPeers = DefaultMaxTrackedPeers
106+ }
94107 return & ChainSelector {
95- config : cfg ,
96- securityParam : cfg .SecurityParam ,
97- peerTips : make (map [ouroboros.ConnectionId ]* PeerChainTip ),
108+ config : cfg ,
109+ securityParam : cfg .SecurityParam ,
110+ maxTrackedPeers : maxPeers ,
111+ peerTips : make (map [ouroboros.ConnectionId ]* PeerChainTip ),
98112 }
99113}
100114
@@ -130,6 +144,7 @@ func (cs *ChainSelector) UpdatePeerTip(
130144) bool {
131145 shouldEvaluate := false
132146 accepted := true
147+ var evictedConn * ouroboros.ConnectionId
133148
134149 func () {
135150 cs .mutex .Lock ()
@@ -160,6 +175,20 @@ func (cs *ChainSelector) UpdatePeerTip(
160175 if peerTip , exists := cs .peerTips [connId ]; exists {
161176 peerTip .UpdateTip (tip , vrfOutput )
162177 } else {
178+ // Evict the least-recently-updated peer if at capacity
179+ if len (cs .peerTips ) >= cs .maxTrackedPeers {
180+ evictedConn = cs .evictLeastRecentPeerLocked ()
181+ if evictedConn == nil {
182+ cs .config .Logger .Warn (
183+ "cannot accept new peer: at capacity and best peer is the only tracked peer" ,
184+ "connection_id" , connId .String (),
185+ "peer_count" , len (cs .peerTips ),
186+ "max_tracked_peers" , cs .maxTrackedPeers ,
187+ )
188+ accepted = false
189+ return
190+ }
191+ }
163192 cs .peerTips [connId ] = NewPeerChainTip (connId , tip , vrfOutput )
164193 }
165194
@@ -183,6 +212,15 @@ func (cs *ChainSelector) UpdatePeerTip(
183212 }
184213 }()
185214
215+ // Publish eviction event outside the lock to prevent deadlock
216+ if evictedConn != nil && cs .config .EventBus != nil {
217+ evt := event .NewEvent (
218+ PeerEvictedEventType ,
219+ PeerEvictedEvent {ConnectionId : * evictedConn },
220+ )
221+ cs .config .EventBus .Publish (PeerEvictedEventType , evt )
222+ }
223+
186224 if ! accepted {
187225 return false
188226 }
@@ -194,6 +232,65 @@ func (cs *ChainSelector) UpdatePeerTip(
194232 return true
195233}
196234
235+ // evictLeastRecentPeerLocked removes the peer with the oldest LastUpdated
236+ // timestamp from the peerTips map. It never evicts the current best peer.
237+ // When multiple peers share the same LastUpdated timestamp (common on
238+ // Windows where clock resolution is ~15ms), the peer with the lowest
239+ // block number is evicted first. If block numbers also tie, the
240+ // connection ID string is used as a final deterministic tie-breaker.
241+ // Returns a pointer to the evicted connection ID, or nil if no eviction
242+ // was possible (e.g. the only tracked peer is the best peer).
243+ // Must be called with cs.mutex held.
244+ func (cs * ChainSelector ) evictLeastRecentPeerLocked () * ouroboros.ConnectionId {
245+ var oldestConn ouroboros.ConnectionId
246+ var oldestTip * PeerChainTip
247+ found := false
248+
249+ for connId , peerTip := range cs .peerTips {
250+ // Never evict the current best peer
251+ if cs .bestPeerConn != nil && * cs .bestPeerConn == connId {
252+ continue
253+ }
254+ if ! found {
255+ oldestConn = connId
256+ oldestTip = peerTip
257+ found = true
258+ continue
259+ }
260+ // Primary: oldest LastUpdated wins eviction
261+ if peerTip .LastUpdated .Before (oldestTip .LastUpdated ) {
262+ oldestConn = connId
263+ oldestTip = peerTip
264+ } else if peerTip .LastUpdated .Equal (oldestTip .LastUpdated ) {
265+ // Tie-break on block number: evict the peer with
266+ // the lower block number (less useful chain)
267+ if peerTip .Tip .BlockNumber < oldestTip .Tip .BlockNumber {
268+ oldestConn = connId
269+ oldestTip = peerTip
270+ } else if peerTip .Tip .BlockNumber == oldestTip .Tip .BlockNumber {
271+ // Final tie-break: deterministic by connection ID
272+ if connId .String () < oldestConn .String () {
273+ oldestConn = connId
274+ oldestTip = peerTip
275+ }
276+ }
277+ }
278+ }
279+
280+ if found {
281+ cs .config .Logger .Debug (
282+ "evicting least-recent peer due to tracking limit" ,
283+ "connection_id" , oldestConn .String (),
284+ "last_updated" , oldestTip .LastUpdated ,
285+ "peer_count" , len (cs .peerTips ),
286+ "max_tracked_peers" , cs .maxTrackedPeers ,
287+ )
288+ delete (cs .peerTips , oldestConn )
289+ return & oldestConn
290+ }
291+ return nil
292+ }
293+
197294// RemovePeer removes a peer from tracking.
198295func (cs * ChainSelector ) RemovePeer (connId ouroboros.ConnectionId ) {
199296 var switchEvent * event.Event
0 commit comments