github.com/prysmaticlabs/prysm@v1.4.4/beacon-chain/p2p/peers/scorers/service.go (about) 1 package scorers 2 3 import ( 4 "context" 5 "math" 6 "time" 7 8 "github.com/libp2p/go-libp2p-core/peer" 9 "github.com/prysmaticlabs/prysm/beacon-chain/p2p/peers/peerdata" 10 ) 11 12 var _ Scorer = (*Service)(nil) 13 14 // ScoreRoundingFactor defines how many digits to keep in decimal part. 15 // This parameter is used in math.Round(score*ScoreRoundingFactor) / ScoreRoundingFactor. 16 const ScoreRoundingFactor = 10000 17 18 // BadPeerScore defines score that is returned for a bad peer (all other metrics are ignored). 19 const BadPeerScore = -1.00 20 21 // Scorer defines minimum set of methods every peer scorer must expose. 22 type Scorer interface { 23 Score(pid peer.ID) float64 24 IsBadPeer(pid peer.ID) bool 25 BadPeers() []peer.ID 26 } 27 28 // Service manages peer scorers that are used to calculate overall peer score. 29 type Service struct { 30 store *peerdata.Store 31 scorers struct { 32 badResponsesScorer *BadResponsesScorer 33 blockProviderScorer *BlockProviderScorer 34 peerStatusScorer *PeerStatusScorer 35 gossipScorer *GossipScorer 36 } 37 weights map[Scorer]float64 38 totalWeight float64 39 } 40 41 // Config holds configuration parameters for scoring service. 42 type Config struct { 43 BadResponsesScorerConfig *BadResponsesScorerConfig 44 BlockProviderScorerConfig *BlockProviderScorerConfig 45 PeerStatusScorerConfig *PeerStatusScorerConfig 46 GossipScorerConfig *GossipScorerConfig 47 } 48 49 // NewService provides fully initialized peer scoring service. 50 func NewService(ctx context.Context, store *peerdata.Store, config *Config) *Service { 51 s := &Service{ 52 store: store, 53 weights: make(map[Scorer]float64), 54 } 55 56 // Register scorers. 57 s.scorers.badResponsesScorer = newBadResponsesScorer(store, config.BadResponsesScorerConfig) 58 s.setScorerWeight(s.scorers.badResponsesScorer, 1.0) 59 s.scorers.blockProviderScorer = newBlockProviderScorer(store, config.BlockProviderScorerConfig) 60 s.setScorerWeight(s.scorers.blockProviderScorer, 1.0) 61 s.scorers.peerStatusScorer = newPeerStatusScorer(store, config.PeerStatusScorerConfig) 62 s.setScorerWeight(s.scorers.peerStatusScorer, 0.0) 63 s.scorers.gossipScorer = newGossipScorer(store, config.GossipScorerConfig) 64 s.setScorerWeight(s.scorers.gossipScorer, 0.0) 65 66 // Start background tasks. 67 go s.loop(ctx) 68 69 return s 70 } 71 72 // BadResponsesScorer exposes bad responses scoring service. 73 func (s *Service) BadResponsesScorer() *BadResponsesScorer { 74 return s.scorers.badResponsesScorer 75 } 76 77 // BlockProviderScorer exposes block provider scoring service. 78 func (s *Service) BlockProviderScorer() *BlockProviderScorer { 79 return s.scorers.blockProviderScorer 80 } 81 82 // PeerStatusScorer exposes peer chain status scoring service. 83 func (s *Service) PeerStatusScorer() *PeerStatusScorer { 84 return s.scorers.peerStatusScorer 85 } 86 87 // GossipScorer exposes the peer's gossip scoring service. 88 func (s *Service) GossipScorer() *GossipScorer { 89 return s.scorers.gossipScorer 90 } 91 92 // ActiveScorersCount returns number of scorers that can affect score (have non-zero weight). 93 func (s *Service) ActiveScorersCount() int { 94 cnt := 0 95 for _, w := range s.weights { 96 if w > 0 { 97 cnt++ 98 } 99 } 100 return cnt 101 } 102 103 // Score returns calculated peer score across all tracked metrics. 104 func (s *Service) Score(pid peer.ID) float64 { 105 s.store.RLock() 106 defer s.store.RUnlock() 107 108 score := float64(0) 109 if _, ok := s.store.PeerData(pid); !ok { 110 return 0 111 } 112 score += s.scorers.badResponsesScorer.score(pid) * s.scorerWeight(s.scorers.badResponsesScorer) 113 score += s.scorers.blockProviderScorer.score(pid) * s.scorerWeight(s.scorers.blockProviderScorer) 114 score += s.scorers.peerStatusScorer.score(pid) * s.scorerWeight(s.scorers.peerStatusScorer) 115 score += s.scorers.gossipScorer.score(pid) * s.scorerWeight(s.scorers.gossipScorer) 116 return math.Round(score*ScoreRoundingFactor) / ScoreRoundingFactor 117 } 118 119 // IsBadPeer traverses all the scorers to see if any of them classifies peer as bad. 120 func (s *Service) IsBadPeer(pid peer.ID) bool { 121 s.store.RLock() 122 defer s.store.RUnlock() 123 return s.isBadPeer(pid) 124 } 125 126 // isBadPeer is a lock-free version of isBadPeer. 127 func (s *Service) isBadPeer(pid peer.ID) bool { 128 if s.scorers.badResponsesScorer.isBadPeer(pid) { 129 return true 130 } 131 if s.scorers.peerStatusScorer.isBadPeer(pid) { 132 return true 133 } 134 // TODO(#6043): Hook in gossip scorer's relevant 135 // method to check if peer has a bad gossip score. 136 return false 137 } 138 139 // BadPeers returns the peers that are considered bad by any of registered scorers. 140 func (s *Service) BadPeers() []peer.ID { 141 s.store.RLock() 142 defer s.store.RUnlock() 143 144 badPeers := make([]peer.ID, 0) 145 for pid := range s.store.Peers() { 146 if s.isBadPeer(pid) { 147 badPeers = append(badPeers, pid) 148 } 149 } 150 return badPeers 151 } 152 153 // ValidationError returns peer data validation error, which potentially provides more information 154 // why peer is considered bad. 155 func (s *Service) ValidationError(pid peer.ID) error { 156 s.store.RLock() 157 defer s.store.RUnlock() 158 159 peerData, ok := s.store.PeerData(pid) 160 if !ok { 161 return nil 162 } 163 return peerData.ChainStateValidationError 164 } 165 166 // loop handles background tasks. 167 func (s *Service) loop(ctx context.Context) { 168 decayBadResponsesStats := time.NewTicker(s.scorers.badResponsesScorer.Params().DecayInterval) 169 defer decayBadResponsesStats.Stop() 170 decayBlockProviderStats := time.NewTicker(s.scorers.blockProviderScorer.Params().DecayInterval) 171 defer decayBlockProviderStats.Stop() 172 173 for { 174 select { 175 case <-decayBadResponsesStats.C: 176 s.scorers.badResponsesScorer.Decay() 177 case <-decayBlockProviderStats.C: 178 s.scorers.blockProviderScorer.Decay() 179 case <-ctx.Done(): 180 return 181 } 182 } 183 } 184 185 // setScorerWeight adds scorer to map of known scorers. 186 func (s *Service) setScorerWeight(scorer Scorer, weight float64) { 187 s.weights[scorer] = weight 188 s.totalWeight += s.weights[scorer] 189 } 190 191 // scorerWeight calculates contribution percentage of a given scorer in total score. 192 func (s *Service) scorerWeight(scorer Scorer) float64 { 193 return s.weights[scorer] / s.totalWeight 194 }