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  }