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  }