github.com/amazechain/amc@v0.1.3/internal/p2p/peers/scorers/block_providers.go (about)

     1  package scorers
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/amazechain/amc/internal/p2p/peers/peerdata"
     6  	"math"
     7  	"math/rand"
     8  	"sort"
     9  	"time"
    10  
    11  	"github.com/libp2p/go-libp2p/core/peer"
    12  )
    13  
    14  var _ Scorer = (*BlockProviderScorer)(nil)
    15  
    16  const (
    17  	// DefaultBlockProviderProcessedBatchWeight is a default reward weight of a processed batch of blocks.
    18  	DefaultBlockProviderProcessedBatchWeight = float64(0.1)
    19  	// DefaultBlockProviderProcessedBlocksCap defines default value for processed blocks cap.
    20  	// e.g. 20 * 64 := 20 batches of size 64 (with 0.05 per batch reward, 20 batches result in score of 1.0).
    21  	DefaultBlockProviderProcessedBlocksCap = uint64(10 * 64)
    22  	// DefaultBlockProviderDecayInterval defines how often the decaying routine is called.
    23  	DefaultBlockProviderDecayInterval = 30 * time.Second
    24  	// DefaultBlockProviderDecay defines default blocks that are to be subtracted from stats on each
    25  	// decay interval. Effectively, this param provides minimum expected performance for a peer to remain
    26  	// high scorer.
    27  	DefaultBlockProviderDecay = uint64(1 * 64)
    28  	// DefaultBlockProviderStalePeerRefreshInterval defines default interval at which peers should be given
    29  	// opportunity to provide blocks (their score gets boosted, up until they are selected for
    30  	// fetching).
    31  	DefaultBlockProviderStalePeerRefreshInterval = 5 * time.Minute
    32  
    33  	//todo
    34  	BlockBatchLimit = 10
    35  )
    36  
    37  // BlockProviderScorer represents block provider scoring service.
    38  type BlockProviderScorer struct {
    39  	config *BlockProviderScorerConfig
    40  	store  *peerdata.Store
    41  	// maxScore is a cached value for maximum attainable block provider score.
    42  	// It is calculated, on startup, as following: (processedBlocksCap / batchSize) * batchWeight.
    43  	maxScore float64
    44  }
    45  
    46  // BlockProviderScorerConfig holds configuration parameters for block providers scoring service.
    47  type BlockProviderScorerConfig struct {
    48  	// ProcessedBatchWeight defines a reward for a single processed batch of blocks.
    49  	ProcessedBatchWeight float64
    50  	// ProcessedBlocksCap defines the highest number of processed blocks that are counted towards peer's score.
    51  	// Once that cap is attained, peer is considered good to fetch from (and several peers having the
    52  	// same score, are picked at random). To stay at max score, peer must continue to perform, as
    53  	// stats decays quickly.
    54  	ProcessedBlocksCap uint64
    55  	// DecayInterval defines how often stats should be decayed.
    56  	DecayInterval time.Duration
    57  	// Decay specifies number of blocks subtracted from stats on each decay step.
    58  	Decay uint64
    59  	// StalePeerRefreshInterval is an interval at which peers should be given an opportunity
    60  	// to provide blocks (scores are boosted to max up until such peers are selected).
    61  	StalePeerRefreshInterval time.Duration
    62  }
    63  
    64  // newBlockProviderScorer creates block provider scoring service.
    65  func newBlockProviderScorer(store *peerdata.Store, config *BlockProviderScorerConfig) *BlockProviderScorer {
    66  	if config == nil {
    67  		config = &BlockProviderScorerConfig{}
    68  	}
    69  	scorer := &BlockProviderScorer{
    70  		config: config,
    71  		store:  store,
    72  	}
    73  	if scorer.config.ProcessedBatchWeight == 0.0 {
    74  		scorer.config.ProcessedBatchWeight = DefaultBlockProviderProcessedBatchWeight
    75  	}
    76  	if scorer.config.DecayInterval == 0 {
    77  		scorer.config.DecayInterval = DefaultBlockProviderDecayInterval
    78  	}
    79  	if scorer.config.ProcessedBlocksCap == 0 {
    80  		scorer.config.ProcessedBlocksCap = DefaultBlockProviderProcessedBlocksCap
    81  	}
    82  	if scorer.config.Decay == 0 {
    83  		scorer.config.Decay = DefaultBlockProviderDecay
    84  	}
    85  	if scorer.config.StalePeerRefreshInterval == 0 {
    86  		scorer.config.StalePeerRefreshInterval = DefaultBlockProviderStalePeerRefreshInterval
    87  	}
    88  	batchSize := uint64(BlockBatchLimit)
    89  	scorer.maxScore = 1.0
    90  	if batchSize > 0 {
    91  		totalBatches := float64(scorer.config.ProcessedBlocksCap / batchSize)
    92  		scorer.maxScore = totalBatches * scorer.config.ProcessedBatchWeight
    93  		scorer.maxScore = math.Round(scorer.maxScore*ScoreRoundingFactor) / ScoreRoundingFactor
    94  	}
    95  	return scorer
    96  }
    97  
    98  // Score calculates and returns block provider score.
    99  func (s *BlockProviderScorer) Score(pid peer.ID) float64 {
   100  	s.store.RLock()
   101  	defer s.store.RUnlock()
   102  	return s.score(pid)
   103  }
   104  
   105  // score is a lock-free version of Score.
   106  func (s *BlockProviderScorer) score(pid peer.ID) float64 {
   107  	score := float64(0)
   108  	peerData, ok := s.store.PeerData(pid)
   109  	// Boost score of new peers or peers that haven't been accessed for too long.
   110  	if !ok || time.Since(peerData.BlockProviderUpdated) >= s.config.StalePeerRefreshInterval {
   111  		return s.maxScore
   112  	}
   113  	batchSize := uint64(BlockBatchLimit)
   114  	if batchSize > 0 {
   115  		processedBatches := float64(peerData.ProcessedBlocks / batchSize)
   116  		score += processedBatches * s.config.ProcessedBatchWeight
   117  	}
   118  	return math.Round(score*ScoreRoundingFactor) / ScoreRoundingFactor
   119  }
   120  
   121  // Params exposes scorer's parameters.
   122  func (s *BlockProviderScorer) Params() *BlockProviderScorerConfig {
   123  	return s.config
   124  }
   125  
   126  // IncrementProcessedBlocks increments the number of blocks that have been successfully processed.
   127  func (s *BlockProviderScorer) IncrementProcessedBlocks(pid peer.ID, cnt uint64) {
   128  	s.store.Lock()
   129  	defer s.store.Unlock()
   130  	defer s.touch(pid)
   131  
   132  	if cnt <= 0 {
   133  		return
   134  	}
   135  	peerData := s.store.PeerDataGetOrCreate(pid)
   136  	if peerData.ProcessedBlocks+cnt > s.config.ProcessedBlocksCap {
   137  		cnt = s.config.ProcessedBlocksCap - peerData.ProcessedBlocks
   138  	}
   139  	if cnt > 0 {
   140  		peerData.ProcessedBlocks += cnt
   141  	}
   142  }
   143  
   144  // Touch updates last access time for a given peer. This allows to detect peers that are
   145  // stale and boost their scores to increase chances in block fetching participation.
   146  func (s *BlockProviderScorer) Touch(pid peer.ID, t ...time.Time) {
   147  	s.store.Lock()
   148  	defer s.store.Unlock()
   149  	s.touch(pid, t...)
   150  }
   151  
   152  // touch is a lock-free version of Touch.
   153  func (s *BlockProviderScorer) touch(pid peer.ID, t ...time.Time) {
   154  	peerData := s.store.PeerDataGetOrCreate(pid)
   155  	if len(t) == 1 {
   156  		peerData.BlockProviderUpdated = t[0]
   157  	} else {
   158  		peerData.BlockProviderUpdated = time.Now()
   159  	}
   160  }
   161  
   162  // ProcessedBlocks returns number of peer returned blocks that are successfully processed.
   163  func (s *BlockProviderScorer) ProcessedBlocks(pid peer.ID) uint64 {
   164  	s.store.RLock()
   165  	defer s.store.RUnlock()
   166  	return s.processedBlocks(pid)
   167  }
   168  
   169  // processedBlocks is a lock-free version of ProcessedBlocks.
   170  func (s *BlockProviderScorer) processedBlocks(pid peer.ID) uint64 {
   171  	if peerData, ok := s.store.PeerData(pid); ok {
   172  		return peerData.ProcessedBlocks
   173  	}
   174  	return 0
   175  }
   176  
   177  // IsBadPeer states if the peer is to be considered bad.
   178  // Block provider scorer cannot guarantee that lower score of a peer is indeed a sign of a bad peer.
   179  // Therefore this scorer never marks peers as bad, and relies on scores to probabilistically sort
   180  // out low-scorers (see WeightSorted method).
   181  func (_ *BlockProviderScorer) IsBadPeer(_ peer.ID) bool {
   182  	return false
   183  }
   184  
   185  // BadPeers returns the peers that are considered bad.
   186  // No peers are considered bad by block providers scorer.
   187  func (_ *BlockProviderScorer) BadPeers() []peer.ID {
   188  	return []peer.ID{}
   189  }
   190  
   191  // Decay updates block provider counters by decaying them.
   192  // This urges peers to keep up the performance to continue getting a high score (and allows
   193  // new peers to contest previously high scoring ones).
   194  func (s *BlockProviderScorer) Decay() {
   195  	s.store.Lock()
   196  	defer s.store.Unlock()
   197  
   198  	for _, peerData := range s.store.Peers() {
   199  		if peerData.ProcessedBlocks > s.config.Decay {
   200  			peerData.ProcessedBlocks -= s.config.Decay
   201  		} else {
   202  			peerData.ProcessedBlocks = 0
   203  		}
   204  	}
   205  }
   206  
   207  // WeightSorted returns a list of block providers weight sorted by score, where items are selected
   208  // probabilistically with more "heavy" items having a higher chance of being picked.
   209  func (s *BlockProviderScorer) WeightSorted(
   210  	r *rand.Rand, pids []peer.ID, scoreFn func(pid peer.ID, score float64) float64,
   211  ) []peer.ID {
   212  	if len(pids) == 0 {
   213  		return pids
   214  	}
   215  	s.store.Lock()
   216  	defer s.store.Unlock()
   217  
   218  	// See http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/ for details.
   219  	nextPID := func(weights map[peer.ID]float64) peer.ID {
   220  		totalWeight := 0
   221  		for _, w := range weights {
   222  			// Factor by 100, to allow weights in (0; 1) range.
   223  			totalWeight += int(w * 100)
   224  		}
   225  		if totalWeight <= 0 {
   226  			return ""
   227  		}
   228  		rnd := r.Intn(totalWeight)
   229  		for pid, w := range weights {
   230  			rnd -= int(w * 100)
   231  			if rnd < 0 {
   232  				return pid
   233  			}
   234  		}
   235  		return ""
   236  	}
   237  
   238  	scores, _ := s.mapScoresAndPeers(pids, scoreFn)
   239  	peers := make([]peer.ID, 0)
   240  	for i := 0; i < len(pids); i++ {
   241  		if pid := nextPID(scores); pid != "" {
   242  			peers = append(peers, pid)
   243  			delete(scores, pid)
   244  		}
   245  	}
   246  	// Left over peers (like peers having zero weight), are added at the end of the list.
   247  	for pid := range scores {
   248  		peers = append(peers, pid)
   249  	}
   250  
   251  	return peers
   252  }
   253  
   254  // Sorted returns a list of block providers sorted by score in descending order.
   255  // When custom scorer function is provided, items are returned in order provided by it.
   256  func (s *BlockProviderScorer) Sorted(
   257  	pids []peer.ID, scoreFn func(pid peer.ID, score float64) float64,
   258  ) []peer.ID {
   259  	if len(pids) == 0 {
   260  		return pids
   261  	}
   262  	s.store.Lock()
   263  	defer s.store.Unlock()
   264  
   265  	scores, peers := s.mapScoresAndPeers(pids, scoreFn)
   266  	sort.Slice(peers, func(i, j int) bool {
   267  		return scores[peers[i]] > scores[peers[j]]
   268  	})
   269  	return peers
   270  }
   271  
   272  // mapScoresAndPeers is a utility function to map peers and their respective scores (using custom
   273  // scoring function if necessary).
   274  func (s *BlockProviderScorer) mapScoresAndPeers(
   275  	pids []peer.ID, scoreFn func(pid peer.ID, score float64) float64,
   276  ) (map[peer.ID]float64, []peer.ID) {
   277  	scores := make(map[peer.ID]float64, len(pids))
   278  	peers := make([]peer.ID, len(pids))
   279  	for i, pid := range pids {
   280  		if scoreFn != nil {
   281  			scores[pid] = scoreFn(pid, s.score(pid))
   282  		} else {
   283  			scores[pid] = s.score(pid)
   284  		}
   285  		peers[i] = pid
   286  	}
   287  	return scores, peers
   288  }
   289  
   290  // FormatScorePretty returns full scoring information in a human-readable format.
   291  func (s *BlockProviderScorer) FormatScorePretty(pid peer.ID) string {
   292  	s.store.RLock()
   293  	defer s.store.RUnlock()
   294  	score := s.score(pid)
   295  	return fmt.Sprintf("[%0.1f%%, raw: %0.2f,  blocks: %d/%d]",
   296  		(score/s.MaxScore())*100, score, s.processedBlocks(pid), s.config.ProcessedBlocksCap)
   297  }
   298  
   299  // MaxScore exposes maximum score attainable by peers.
   300  func (s *BlockProviderScorer) MaxScore() float64 {
   301  	return s.maxScore
   302  }