github.com/vipernet-xyz/tm@v0.34.24/p2p/trust/metric.go (about)

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