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  }