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