You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

383 lines
11 KiB

  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. package trust
  4. import (
  5. "math"
  6. "sync"
  7. "time"
  8. )
  9. //---------------------------------------------------------------------------------------
  10. const (
  11. // The weight applied to the derivative when current behavior is >= previous behavior
  12. defaultDerivativeGamma1 = 0
  13. // The weight applied to the derivative when current behavior is less than previous behavior
  14. defaultDerivativeGamma2 = 1.0
  15. // The weight applied to history data values when calculating the history value
  16. defaultHistoryDataWeight = 0.8
  17. )
  18. // MetricHistoryJSON - history data necessary to save the trust metric
  19. type MetricHistoryJSON struct {
  20. NumIntervals int `json:"intervals"`
  21. History []float64 `json:"history"`
  22. }
  23. // TrustMetric - keeps track of peer reliability
  24. // See tendermint/docs/architecture/adr-006-trust-metric.md for details
  25. type TrustMetric struct {
  26. // Mutex that protects the metric from concurrent access
  27. mtx sync.Mutex
  28. // Determines the percentage given to current behavior
  29. proportionalWeight float64
  30. // Determines the percentage given to prior behavior
  31. integralWeight float64
  32. // Count of how many time intervals this metric has been tracking
  33. numIntervals int
  34. // Size of the time interval window for this trust metric
  35. maxIntervals int
  36. // The time duration for a single time interval
  37. intervalLen time.Duration
  38. // Stores the trust history data for this metric
  39. history []float64
  40. // Weights applied to the history data when calculating the history value
  41. historyWeights []float64
  42. // The sum of the history weights used when calculating the history value
  43. historyWeightSum float64
  44. // The current number of history data elements
  45. historySize int
  46. // The maximum number of history data elements
  47. historyMaxSize int
  48. // The calculated history value for the current time interval
  49. historyValue float64
  50. // The number of recorded good and bad events for the current time interval
  51. bad, good float64
  52. // While true, history data is not modified
  53. paused bool
  54. // Signal channel for stopping the trust metric go-routine
  55. stop chan struct{}
  56. }
  57. // NewMetric returns a trust metric with the default configuration
  58. func NewMetric() *TrustMetric {
  59. return NewMetricWithConfig(DefaultConfig())
  60. }
  61. // NewMetricWithConfig returns a trust metric with a custom configuration
  62. func NewMetricWithConfig(tmc TrustMetricConfig) *TrustMetric {
  63. tm := new(TrustMetric)
  64. config := customConfig(tmc)
  65. // Setup using the configuration values
  66. tm.proportionalWeight = config.ProportionalWeight
  67. tm.integralWeight = config.IntegralWeight
  68. tm.intervalLen = config.IntervalLength
  69. // The maximum number of time intervals is the tracking window / interval length
  70. tm.maxIntervals = int(config.TrackingWindow / tm.intervalLen)
  71. // The history size will be determined by the maximum number of time intervals
  72. tm.historyMaxSize = intervalToHistoryOffset(tm.maxIntervals) + 1
  73. // This metric has a perfect history so far
  74. tm.historyValue = 1.0
  75. // Setup the stop channel
  76. tm.stop = make(chan struct{})
  77. go tm.processRequests()
  78. return tm
  79. }
  80. // Returns a snapshot of the trust metric history data
  81. func (tm *TrustMetric) HistoryJSON() MetricHistoryJSON {
  82. tm.mtx.Lock()
  83. defer tm.mtx.Unlock()
  84. return MetricHistoryJSON{
  85. NumIntervals: tm.numIntervals,
  86. History: tm.history,
  87. }
  88. }
  89. // Instantiates a trust metric by loading the history data for a single peer.
  90. // This is called only once and only right after creation, which is why the
  91. // lock is not held while accessing the trust metric struct members
  92. func (tm *TrustMetric) Init(hist MetricHistoryJSON) {
  93. // Restore the number of time intervals we have previously tracked
  94. if hist.NumIntervals > tm.maxIntervals {
  95. hist.NumIntervals = tm.maxIntervals
  96. }
  97. tm.numIntervals = hist.NumIntervals
  98. // Restore the history and its current size
  99. if len(hist.History) > tm.historyMaxSize {
  100. // Keep the history no larger than historyMaxSize
  101. last := len(hist.History) - tm.historyMaxSize
  102. hist.History = hist.History[last:]
  103. }
  104. tm.history = hist.History
  105. tm.historySize = len(tm.history)
  106. // Create the history weight values and weight sum
  107. for i := 1; i <= tm.numIntervals; i++ {
  108. x := math.Pow(defaultHistoryDataWeight, float64(i)) // Optimistic weight
  109. tm.historyWeights = append(tm.historyWeights, x)
  110. }
  111. for _, v := range tm.historyWeights {
  112. tm.historyWeightSum += v
  113. }
  114. // Calculate the history value based on the loaded history data
  115. tm.historyValue = tm.calcHistoryValue()
  116. }
  117. // Pause tells the metric to pause recording data over time intervals.
  118. // All method calls that indicate events will unpause the metric
  119. func (tm *TrustMetric) Pause() {
  120. tm.mtx.Lock()
  121. defer tm.mtx.Unlock()
  122. // Pause the metric for now
  123. tm.paused = true
  124. }
  125. // Stop tells the metric to stop recording data over time intervals
  126. func (tm *TrustMetric) Stop() {
  127. tm.stop <- struct{}{}
  128. }
  129. // BadEvents indicates that an undesirable event(s) took place
  130. func (tm *TrustMetric) BadEvents(num int) {
  131. tm.mtx.Lock()
  132. defer tm.mtx.Unlock()
  133. tm.unpause()
  134. tm.bad += float64(num)
  135. }
  136. // GoodEvents indicates that a desirable event(s) took place
  137. func (tm *TrustMetric) GoodEvents(num int) {
  138. tm.mtx.Lock()
  139. defer tm.mtx.Unlock()
  140. tm.unpause()
  141. tm.good += float64(num)
  142. }
  143. // TrustValue gets the dependable trust value; always between 0 and 1
  144. func (tm *TrustMetric) TrustValue() float64 {
  145. tm.mtx.Lock()
  146. defer tm.mtx.Unlock()
  147. return tm.calcTrustValue()
  148. }
  149. // TrustScore gets a score based on the trust value always between 0 and 100
  150. func (tm *TrustMetric) TrustScore() int {
  151. score := tm.TrustValue() * 100
  152. return int(math.Floor(score))
  153. }
  154. // NextTimeInterval saves current time interval data and prepares for the following interval
  155. func (tm *TrustMetric) NextTimeInterval() {
  156. tm.mtx.Lock()
  157. defer tm.mtx.Unlock()
  158. if tm.paused {
  159. // Do not prepare for the next time interval while paused
  160. return
  161. }
  162. // Add the current trust value to the history data
  163. newHist := tm.calcTrustValue()
  164. tm.history = append(tm.history, newHist)
  165. // Update history and interval counters
  166. if tm.historySize < tm.historyMaxSize {
  167. tm.historySize++
  168. } else {
  169. // Keep the history no larger than historyMaxSize
  170. last := len(tm.history) - tm.historyMaxSize
  171. tm.history = tm.history[last:]
  172. }
  173. if tm.numIntervals < tm.maxIntervals {
  174. tm.numIntervals++
  175. // Add the optimistic weight for the new time interval
  176. wk := math.Pow(defaultHistoryDataWeight, float64(tm.numIntervals))
  177. tm.historyWeights = append(tm.historyWeights, wk)
  178. tm.historyWeightSum += wk
  179. }
  180. // Update the history data using Faded Memories
  181. tm.updateFadedMemory()
  182. // Calculate the history value for the upcoming time interval
  183. tm.historyValue = tm.calcHistoryValue()
  184. tm.good = 0
  185. tm.bad = 0
  186. }
  187. // Copy returns a new trust metric with members containing the same values
  188. func (tm *TrustMetric) Copy() *TrustMetric {
  189. tm.mtx.Lock()
  190. defer tm.mtx.Unlock()
  191. if tm == nil {
  192. return nil
  193. }
  194. return &TrustMetric{
  195. proportionalWeight: tm.proportionalWeight,
  196. integralWeight: tm.integralWeight,
  197. numIntervals: tm.numIntervals,
  198. maxIntervals: tm.maxIntervals,
  199. intervalLen: tm.intervalLen,
  200. history: tm.history,
  201. historyWeights: tm.historyWeights,
  202. historyWeightSum: tm.historyWeightSum,
  203. historySize: tm.historySize,
  204. historyMaxSize: tm.historyMaxSize,
  205. historyValue: tm.historyValue,
  206. good: tm.good,
  207. bad: tm.bad,
  208. paused: tm.paused,
  209. stop: make(chan struct{}),
  210. }
  211. }
  212. /* Private methods */
  213. // This method is for a goroutine that handles all requests on the metric
  214. func (tm *TrustMetric) processRequests() {
  215. t := time.NewTicker(tm.intervalLen)
  216. defer t.Stop()
  217. loop:
  218. for {
  219. select {
  220. case <-t.C:
  221. tm.NextTimeInterval()
  222. case <-tm.stop:
  223. // Stop all further tracking for this metric
  224. break loop
  225. }
  226. }
  227. }
  228. // Wakes the trust metric up if it is currently paused
  229. // This method needs to be called with the mutex locked
  230. func (tm *TrustMetric) unpause() {
  231. // Check if this is the first experience with
  232. // what we are tracking since being paused
  233. if tm.paused {
  234. tm.good = 0
  235. tm.bad = 0
  236. // New events cause us to unpause the metric
  237. tm.paused = false
  238. }
  239. }
  240. // Calculates the trust value for the request processing
  241. func (tm *TrustMetric) calcTrustValue() float64 {
  242. weightedP := tm.proportionalWeight * tm.proportionalValue()
  243. weightedI := tm.integralWeight * tm.historyValue
  244. weightedD := tm.weightedDerivative()
  245. tv := weightedP + weightedI + weightedD
  246. // Do not return a negative value.
  247. if tv < 0 {
  248. tv = 0
  249. }
  250. return tv
  251. }
  252. // Calculates the current score for good/bad experiences
  253. func (tm *TrustMetric) proportionalValue() float64 {
  254. value := 1.0
  255. total := tm.good + tm.bad
  256. if total > 0 {
  257. value = tm.good / total
  258. }
  259. return value
  260. }
  261. // Strengthens the derivative component when the change is negative
  262. func (tm *TrustMetric) weightedDerivative() float64 {
  263. var weight float64 = defaultDerivativeGamma1
  264. d := tm.derivativeValue()
  265. if d < 0 {
  266. weight = defaultDerivativeGamma2
  267. }
  268. return weight * d
  269. }
  270. // Calculates the derivative component
  271. func (tm *TrustMetric) derivativeValue() float64 {
  272. return tm.proportionalValue() - tm.historyValue
  273. }
  274. // Calculates the integral (history) component of the trust value
  275. func (tm *TrustMetric) calcHistoryValue() float64 {
  276. var hv float64
  277. for i := 0; i < tm.numIntervals; i++ {
  278. hv += tm.fadedMemoryValue(i) * tm.historyWeights[i]
  279. }
  280. return hv / tm.historyWeightSum
  281. }
  282. // Retrieves the actual history data value that represents the requested time interval
  283. func (tm *TrustMetric) fadedMemoryValue(interval int) float64 {
  284. first := tm.historySize - 1
  285. if interval == 0 {
  286. // Base case
  287. return tm.history[first]
  288. }
  289. offset := intervalToHistoryOffset(interval)
  290. return tm.history[first-offset]
  291. }
  292. // Performs the update for our Faded Memories process, which allows the
  293. // trust metric tracking window to be large while maintaining a small
  294. // number of history data values
  295. func (tm *TrustMetric) updateFadedMemory() {
  296. if tm.historySize < 2 {
  297. return
  298. }
  299. end := tm.historySize - 1
  300. // Keep the most recent history element
  301. for count := 1; count < tm.historySize; count++ {
  302. i := end - count
  303. // The older the data is, the more we spread it out
  304. x := math.Pow(2, float64(count))
  305. // Two history data values are merged into a single value
  306. tm.history[i] = ((tm.history[i] * (x - 1)) + tm.history[i+1]) / x
  307. }
  308. }
  309. // Map the interval value down to an offset from the beginning of history
  310. func intervalToHistoryOffset(interval int) int {
  311. // The system maintains 2^m interval values in the form of m history
  312. // data values. Therefore, we access the ith interval by obtaining
  313. // the history data index = the floor of log2(i)
  314. return int(math.Floor(math.Log2(float64(interval))))
  315. }