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.

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