github.com/ethereum-optimism/optimism@v1.7.2/op-node/p2p/monitor/peer_monitor.go (about) 1 package monitor 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 "github.com/ethereum-optimism/optimism/op-service/clock" 10 "github.com/ethereum/go-ethereum/log" 11 "github.com/libp2p/go-libp2p/core/peer" 12 ) 13 14 const ( 15 // Time delay between checking the score of each peer to avoid activity spikes 16 checkInterval = 1 * time.Second 17 ) 18 19 //go:generate mockery --name PeerManager --output mocks/ --with-expecter=true 20 type PeerManager interface { 21 Peers() []peer.ID 22 GetPeerScore(id peer.ID) (float64, error) 23 IsStatic(peer.ID) bool 24 // BanPeer bans the peer until the specified time and disconnects any existing connections. 25 BanPeer(peer.ID, time.Time) error 26 } 27 28 // PeerMonitor runs a background process to periodically check for peers with scores below a minimum. 29 // When it finds bad peers, it disconnects and bans them. 30 // A delay is introduced between each peer being checked to avoid spikes in system load. 31 type PeerMonitor struct { 32 ctx context.Context 33 cancelFn context.CancelFunc 34 l log.Logger 35 clock clock.Clock 36 manager PeerManager 37 minScore float64 38 banDuration time.Duration 39 40 bgTasks sync.WaitGroup 41 42 // Used by checkNextPeer and must only be accessed from the background thread 43 peerList []peer.ID 44 nextPeerIdx int 45 } 46 47 func NewPeerMonitor(ctx context.Context, l log.Logger, clock clock.Clock, manager PeerManager, minScore float64, banDuration time.Duration) *PeerMonitor { 48 ctx, cancelFn := context.WithCancel(ctx) 49 return &PeerMonitor{ 50 ctx: ctx, 51 cancelFn: cancelFn, 52 l: l, 53 clock: clock, 54 manager: manager, 55 minScore: minScore, 56 banDuration: banDuration, 57 } 58 } 59 func (p *PeerMonitor) Start() { 60 p.bgTasks.Add(1) 61 go p.background(p.checkNextPeer) 62 } 63 64 func (p *PeerMonitor) Stop() { 65 p.cancelFn() 66 p.bgTasks.Wait() 67 } 68 69 // checkNextPeer checks the next peer and disconnects and bans it if its score is too low and its not protected. 70 // The first call gets the list of current peers and checks the first one, then each subsequent call checks the next 71 // peer in the list. When the end of the list is reached, an updated list of connected peers is retrieved and the process 72 // starts again. 73 func (p *PeerMonitor) checkNextPeer() error { 74 // Get a new list of peers to check if we've checked all peers in the previous list 75 if p.nextPeerIdx >= len(p.peerList) { 76 p.peerList = p.manager.Peers() 77 p.nextPeerIdx = 0 78 } 79 if len(p.peerList) == 0 { 80 // No peers to check 81 return nil 82 } 83 id := p.peerList[p.nextPeerIdx] 84 p.nextPeerIdx++ 85 score, err := p.manager.GetPeerScore(id) 86 if err != nil { 87 return fmt.Errorf("retrieve score for peer %v: %w", id, err) 88 } 89 if score >= p.minScore { 90 return nil 91 } 92 if p.manager.IsStatic(id) { 93 return nil 94 } 95 if err := p.manager.BanPeer(id, p.clock.Now().Add(p.banDuration)); err != nil { 96 return fmt.Errorf("banning peer %v: %w", id, err) 97 } 98 99 return nil 100 } 101 102 // background is intended to run as a separate go routine. It will call the supplied action function every checkInterval 103 // until the context is done. 104 func (p *PeerMonitor) background(action func() error) { 105 defer p.bgTasks.Done() 106 ticker := p.clock.NewTicker(checkInterval) 107 defer ticker.Stop() 108 for { 109 select { 110 case <-p.ctx.Done(): 111 return 112 case <-ticker.Ch(): 113 if err := action(); err != nil { 114 p.l.Warn("Error while checking connected peer score", "err", err) 115 } 116 } 117 } 118 }