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 }