github.com/badrootd/celestia-core@v0.0.0-20240305091328-aa4207a4b25d/p2p/trust/metric.go (about) 1 package trust 2 3 import ( 4 "math" 5 "time" 6 7 "github.com/badrootd/celestia-core/libs/service" 8 cmtsync "github.com/badrootd/celestia-core/libs/sync" 9 ) 10 11 //--------------------------------------------------------------------------------------- 12 13 const ( 14 // The weight applied to the derivative when current behavior is >= previous behavior 15 defaultDerivativeGamma1 = 0 16 17 // The weight applied to the derivative when current behavior is less than previous behavior 18 defaultDerivativeGamma2 = 1.0 19 20 // The weight applied to history data values when calculating the history value 21 defaultHistoryDataWeight = 0.8 22 ) 23 24 // MetricHistoryJSON - history data necessary to save the trust metric 25 type MetricHistoryJSON struct { 26 NumIntervals int `json:"intervals"` 27 History []float64 `json:"history"` 28 } 29 30 // Metric - keeps track of peer reliability 31 // See cometbft/docs/architecture/adr-006-trust-metric.md for details 32 type Metric struct { 33 service.BaseService 34 35 // Mutex that protects the metric from concurrent access 36 mtx cmtsync.Mutex 37 38 // Determines the percentage given to current behavior 39 proportionalWeight float64 40 41 // Determines the percentage given to prior behavior 42 integralWeight float64 43 44 // Count of how many time intervals this metric has been tracking 45 numIntervals int 46 47 // Size of the time interval window for this trust metric 48 maxIntervals int 49 50 // The time duration for a single time interval 51 intervalLen time.Duration 52 53 // Stores the trust history data for this metric 54 history []float64 55 56 // Weights applied to the history data when calculating the history value 57 historyWeights []float64 58 59 // The sum of the history weights used when calculating the history value 60 historyWeightSum float64 61 62 // The current number of history data elements 63 historySize int 64 65 // The maximum number of history data elements 66 historyMaxSize int 67 68 // The calculated history value for the current time interval 69 historyValue float64 70 71 // The number of recorded good and bad events for the current time interval 72 bad, good float64 73 74 // While true, history data is not modified 75 paused bool 76 77 // Used during testing in order to control the passing of time intervals 78 testTicker MetricTicker 79 } 80 81 // NewMetric returns a trust metric with the default configuration. 82 // Use Start to begin tracking the quality of peer behavior over time 83 func NewMetric() *Metric { 84 return NewMetricWithConfig(DefaultConfig()) 85 } 86 87 // NewMetricWithConfig returns a trust metric with a custom configuration. 88 // Use Start to begin tracking the quality of peer behavior over time 89 func NewMetricWithConfig(tmc MetricConfig) *Metric { 90 tm := new(Metric) 91 config := customConfig(tmc) 92 93 // Setup using the configuration values 94 tm.proportionalWeight = config.ProportionalWeight 95 tm.integralWeight = config.IntegralWeight 96 tm.intervalLen = config.IntervalLength 97 // The maximum number of time intervals is the tracking window / interval length 98 tm.maxIntervals = int(config.TrackingWindow / tm.intervalLen) 99 // The history size will be determined by the maximum number of time intervals 100 tm.historyMaxSize = intervalToHistoryOffset(tm.maxIntervals) + 1 101 // This metric has a perfect history so far 102 tm.historyValue = 1.0 103 104 tm.BaseService = *service.NewBaseService(nil, "Metric", tm) 105 return tm 106 } 107 108 // OnStart implements Service 109 func (tm *Metric) OnStart() error { 110 if err := tm.BaseService.OnStart(); err != nil { 111 return err 112 } 113 go tm.processRequests() 114 return nil 115 } 116 117 // OnStop implements Service 118 // Nothing to do since the goroutine shuts down by itself via BaseService.Quit() 119 func (tm *Metric) OnStop() {} 120 121 // Returns a snapshot of the trust metric history data 122 func (tm *Metric) HistoryJSON() MetricHistoryJSON { 123 tm.mtx.Lock() 124 defer tm.mtx.Unlock() 125 126 return MetricHistoryJSON{ 127 NumIntervals: tm.numIntervals, 128 History: tm.history, 129 } 130 } 131 132 // Instantiates a trust metric by loading the history data for a single peer. 133 // This is called only once and only right after creation, which is why the 134 // lock is not held while accessing the trust metric struct members 135 func (tm *Metric) Init(hist MetricHistoryJSON) { 136 // Restore the number of time intervals we have previously tracked 137 if hist.NumIntervals > tm.maxIntervals { 138 hist.NumIntervals = tm.maxIntervals 139 } 140 tm.numIntervals = hist.NumIntervals 141 // Restore the history and its current size 142 if len(hist.History) > tm.historyMaxSize { 143 // Keep the history no larger than historyMaxSize 144 last := len(hist.History) - tm.historyMaxSize 145 hist.History = hist.History[last:] 146 } 147 tm.history = hist.History 148 tm.historySize = len(tm.history) 149 // Create the history weight values and weight sum 150 for i := 1; i <= tm.numIntervals; i++ { 151 x := math.Pow(defaultHistoryDataWeight, float64(i)) // Optimistic weight 152 tm.historyWeights = append(tm.historyWeights, x) 153 } 154 155 for _, v := range tm.historyWeights { 156 tm.historyWeightSum += v 157 } 158 // Calculate the history value based on the loaded history data 159 tm.historyValue = tm.calcHistoryValue() 160 } 161 162 // Pause tells the metric to pause recording data over time intervals. 163 // All method calls that indicate events will unpause the metric 164 func (tm *Metric) Pause() { 165 tm.mtx.Lock() 166 defer tm.mtx.Unlock() 167 168 // Pause the metric for now 169 tm.paused = true 170 } 171 172 // BadEvents indicates that an undesirable event(s) took place 173 func (tm *Metric) BadEvents(num int) { 174 tm.mtx.Lock() 175 defer tm.mtx.Unlock() 176 177 tm.unpause() 178 tm.bad += float64(num) 179 } 180 181 // GoodEvents indicates that a desirable event(s) took place 182 func (tm *Metric) GoodEvents(num int) { 183 tm.mtx.Lock() 184 defer tm.mtx.Unlock() 185 186 tm.unpause() 187 tm.good += float64(num) 188 } 189 190 // TrustValue gets the dependable trust value; always between 0 and 1 191 func (tm *Metric) TrustValue() float64 { 192 tm.mtx.Lock() 193 defer tm.mtx.Unlock() 194 195 return tm.calcTrustValue() 196 } 197 198 // TrustScore gets a score based on the trust value always between 0 and 100 199 func (tm *Metric) TrustScore() int { 200 score := tm.TrustValue() * 100 201 202 return int(math.Floor(score)) 203 } 204 205 // NextTimeInterval saves current time interval data and prepares for the following interval 206 func (tm *Metric) NextTimeInterval() { 207 tm.mtx.Lock() 208 defer tm.mtx.Unlock() 209 210 if tm.paused { 211 // Do not prepare for the next time interval while paused 212 return 213 } 214 215 // Add the current trust value to the history data 216 newHist := tm.calcTrustValue() 217 tm.history = append(tm.history, newHist) 218 219 // Update history and interval counters 220 if tm.historySize < tm.historyMaxSize { 221 tm.historySize++ 222 } else { 223 // Keep the history no larger than historyMaxSize 224 last := len(tm.history) - tm.historyMaxSize 225 tm.history = tm.history[last:] 226 } 227 228 if tm.numIntervals < tm.maxIntervals { 229 tm.numIntervals++ 230 // Add the optimistic weight for the new time interval 231 wk := math.Pow(defaultHistoryDataWeight, float64(tm.numIntervals)) 232 tm.historyWeights = append(tm.historyWeights, wk) 233 tm.historyWeightSum += wk 234 } 235 236 // Update the history data using Faded Memories 237 tm.updateFadedMemory() 238 // Calculate the history value for the upcoming time interval 239 tm.historyValue = tm.calcHistoryValue() 240 tm.good = 0 241 tm.bad = 0 242 } 243 244 // SetTicker allows a TestTicker to be provided that will manually control 245 // the passing of time from the perspective of the Metric. 246 // The ticker must be set before Start is called on the metric 247 func (tm *Metric) SetTicker(ticker MetricTicker) { 248 tm.mtx.Lock() 249 defer tm.mtx.Unlock() 250 251 tm.testTicker = ticker 252 } 253 254 // Copy returns a new trust metric with members containing the same values 255 func (tm *Metric) Copy() *Metric { 256 if tm == nil { 257 return nil 258 } 259 260 tm.mtx.Lock() 261 defer tm.mtx.Unlock() 262 263 return &Metric{ 264 proportionalWeight: tm.proportionalWeight, 265 integralWeight: tm.integralWeight, 266 numIntervals: tm.numIntervals, 267 maxIntervals: tm.maxIntervals, 268 intervalLen: tm.intervalLen, 269 history: tm.history, 270 historyWeights: tm.historyWeights, 271 historyWeightSum: tm.historyWeightSum, 272 historySize: tm.historySize, 273 historyMaxSize: tm.historyMaxSize, 274 historyValue: tm.historyValue, 275 good: tm.good, 276 bad: tm.bad, 277 paused: tm.paused, 278 } 279 280 } 281 282 /* Private methods */ 283 284 // This method is for a goroutine that handles all requests on the metric 285 func (tm *Metric) processRequests() { 286 t := tm.testTicker 287 if t == nil { 288 // No test ticker was provided, so we create a normal ticker 289 t = NewTicker(tm.intervalLen) 290 } 291 defer t.Stop() 292 // Obtain the raw channel 293 tick := t.GetChannel() 294 loop: 295 for { 296 select { 297 case <-tick: 298 tm.NextTimeInterval() 299 case <-tm.Quit(): 300 // Stop all further tracking for this metric 301 break loop 302 } 303 } 304 } 305 306 // Wakes the trust metric up if it is currently paused 307 // This method needs to be called with the mutex locked 308 func (tm *Metric) unpause() { 309 // Check if this is the first experience with 310 // what we are tracking since being paused 311 if tm.paused { 312 tm.good = 0 313 tm.bad = 0 314 // New events cause us to unpause the metric 315 tm.paused = false 316 } 317 } 318 319 // Calculates the trust value for the request processing 320 func (tm *Metric) calcTrustValue() float64 { 321 weightedP := tm.proportionalWeight * tm.proportionalValue() 322 weightedI := tm.integralWeight * tm.historyValue 323 weightedD := tm.weightedDerivative() 324 325 tv := weightedP + weightedI + weightedD 326 // Do not return a negative value. 327 if tv < 0 { 328 tv = 0 329 } 330 return tv 331 } 332 333 // Calculates the current score for good/bad experiences 334 func (tm *Metric) proportionalValue() float64 { 335 value := 1.0 336 337 total := tm.good + tm.bad 338 if total > 0 { 339 value = tm.good / total 340 } 341 return value 342 } 343 344 // Strengthens the derivative component when the change is negative 345 func (tm *Metric) weightedDerivative() float64 { 346 var weight float64 = defaultDerivativeGamma1 347 348 d := tm.derivativeValue() 349 if d < 0 { 350 weight = defaultDerivativeGamma2 351 } 352 return weight * d 353 } 354 355 // Calculates the derivative component 356 func (tm *Metric) derivativeValue() float64 { 357 return tm.proportionalValue() - tm.historyValue 358 } 359 360 // Calculates the integral (history) component of the trust value 361 func (tm *Metric) calcHistoryValue() float64 { 362 var hv float64 363 364 for i := 0; i < tm.numIntervals; i++ { 365 hv += tm.fadedMemoryValue(i) * tm.historyWeights[i] 366 } 367 368 return hv / tm.historyWeightSum 369 } 370 371 // Retrieves the actual history data value that represents the requested time interval 372 func (tm *Metric) fadedMemoryValue(interval int) float64 { 373 first := tm.historySize - 1 374 375 if interval == 0 { 376 // Base case 377 return tm.history[first] 378 } 379 380 offset := intervalToHistoryOffset(interval) 381 return tm.history[first-offset] 382 } 383 384 // Performs the update for our Faded Memories process, which allows the 385 // trust metric tracking window to be large while maintaining a small 386 // number of history data values 387 func (tm *Metric) updateFadedMemory() { 388 if tm.historySize < 2 { 389 return 390 } 391 392 end := tm.historySize - 1 393 // Keep the most recent history element 394 for count := 1; count < tm.historySize; count++ { 395 i := end - count 396 // The older the data is, the more we spread it out 397 x := math.Pow(2, float64(count)) 398 // Two history data values are merged into a single value 399 tm.history[i] = ((tm.history[i] * (x - 1)) + tm.history[i+1]) / x 400 } 401 } 402 403 // Map the interval value down to an offset from the beginning of history 404 func intervalToHistoryOffset(interval int) int { 405 // The system maintains 2^m interval values in the form of m history 406 // data values. Therefore, we access the ith interval by obtaining 407 // the history data index = the floor of log2(i) 408 return int(math.Floor(math.Log2(float64(interval)))) 409 }