// Copyright 2017 Tendermint. All rights reserved.
|
|
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
|
|
|
|
package trust
|
|
|
|
import (
|
|
"encoding/json"
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
cmn "github.com/tendermint/tmlibs/common"
|
|
dbm "github.com/tendermint/tmlibs/db"
|
|
)
|
|
|
|
const defaultStorePeriodicSaveInterval = 1 * time.Minute
|
|
|
|
// TrustMetricStore - Manages all trust metrics for peers
|
|
type TrustMetricStore struct {
|
|
cmn.BaseService
|
|
|
|
// Maps a Peer.Key to that peer's TrustMetric
|
|
peerMetrics map[string]*TrustMetric
|
|
|
|
// Mutex that protects the map and history data file
|
|
mtx sync.Mutex
|
|
|
|
// The db where peer trust metric history data will be stored
|
|
db dbm.DB
|
|
|
|
// This configuration will be used when creating new TrustMetrics
|
|
config TrustMetricConfig
|
|
}
|
|
|
|
// NewTrustMetricStore returns a store that saves data to the DB
|
|
// and uses the config when creating new trust metrics
|
|
func NewTrustMetricStore(db dbm.DB, tmc TrustMetricConfig) *TrustMetricStore {
|
|
tms := &TrustMetricStore{
|
|
peerMetrics: make(map[string]*TrustMetric),
|
|
db: db,
|
|
config: tmc,
|
|
}
|
|
|
|
tms.BaseService = *cmn.NewBaseService(nil, "TrustMetricStore", tms)
|
|
return tms
|
|
}
|
|
|
|
// OnStart implements Service
|
|
func (tms *TrustMetricStore) OnStart() error {
|
|
tms.BaseService.OnStart()
|
|
|
|
tms.mtx.Lock()
|
|
defer tms.mtx.Unlock()
|
|
|
|
tms.loadFromDB()
|
|
go tms.saveRoutine()
|
|
return nil
|
|
}
|
|
|
|
// OnStop implements Service
|
|
func (tms *TrustMetricStore) OnStop() {
|
|
tms.BaseService.OnStop()
|
|
|
|
tms.mtx.Lock()
|
|
defer tms.mtx.Unlock()
|
|
|
|
// Stop all trust metric go-routines
|
|
for _, tm := range tms.peerMetrics {
|
|
tm.Stop()
|
|
}
|
|
|
|
// Make the final trust history data save
|
|
tms.saveToDB()
|
|
}
|
|
|
|
// Size returns the number of entries in the trust metric store
|
|
func (tms *TrustMetricStore) Size() int {
|
|
tms.mtx.Lock()
|
|
defer tms.mtx.Unlock()
|
|
|
|
return tms.size()
|
|
}
|
|
|
|
// GetPeerTrustMetric returns a trust metric by peer key
|
|
func (tms *TrustMetricStore) GetPeerTrustMetric(key string) *TrustMetric {
|
|
tms.mtx.Lock()
|
|
defer tms.mtx.Unlock()
|
|
|
|
tm, ok := tms.peerMetrics[key]
|
|
if !ok {
|
|
// If the metric is not available, we will create it
|
|
tm = NewMetricWithConfig(tms.config)
|
|
// The metric needs to be in the map
|
|
tms.peerMetrics[key] = tm
|
|
}
|
|
return tm
|
|
}
|
|
|
|
// PeerDisconnected pauses the trust metric associated with the peer identified by the key
|
|
func (tms *TrustMetricStore) PeerDisconnected(key string) {
|
|
tms.mtx.Lock()
|
|
defer tms.mtx.Unlock()
|
|
|
|
// If the Peer that disconnected has a metric, pause it
|
|
if tm, ok := tms.peerMetrics[key]; ok {
|
|
tm.Pause()
|
|
}
|
|
}
|
|
|
|
// Saves the history data for all peers to the store DB.
|
|
// This public method acquires the trust metric store lock
|
|
func (tms *TrustMetricStore) SaveToDB() {
|
|
tms.mtx.Lock()
|
|
defer tms.mtx.Unlock()
|
|
|
|
tms.saveToDB()
|
|
}
|
|
|
|
/* Private methods */
|
|
|
|
// size returns the number of entries in the store without acquiring the mutex
|
|
func (tms *TrustMetricStore) size() int {
|
|
return len(tms.peerMetrics)
|
|
}
|
|
|
|
/* Loading & Saving */
|
|
/* Both loadFromDB and savetoDB assume the mutex has been acquired */
|
|
|
|
var trustMetricKey = []byte("trustMetricStore")
|
|
|
|
// Loads the history data for all peers from the store DB
|
|
// cmn.Panics if file is corrupt
|
|
func (tms *TrustMetricStore) loadFromDB() bool {
|
|
// Obtain the history data we have so far
|
|
bytes := tms.db.Get(trustMetricKey)
|
|
if bytes == nil {
|
|
return false
|
|
}
|
|
|
|
peers := make(map[string]MetricHistoryJSON, 0)
|
|
err := json.Unmarshal(bytes, &peers)
|
|
if err != nil {
|
|
cmn.PanicCrisis(cmn.Fmt("Could not unmarshal Trust Metric Store DB data: %v", err))
|
|
}
|
|
|
|
// If history data exists in the file,
|
|
// load it into trust metric
|
|
for key, p := range peers {
|
|
tm := NewMetricWithConfig(tms.config)
|
|
|
|
tm.Init(p)
|
|
// Load the peer trust metric into the store
|
|
tms.peerMetrics[key] = tm
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Saves the history data for all peers to the store DB
|
|
func (tms *TrustMetricStore) saveToDB() {
|
|
tms.Logger.Debug("Saving TrustHistory to DB", "size", tms.size())
|
|
|
|
peers := make(map[string]MetricHistoryJSON, 0)
|
|
|
|
for key, tm := range tms.peerMetrics {
|
|
// Add an entry for the peer identified by key
|
|
peers[key] = tm.HistoryJSON()
|
|
}
|
|
|
|
// Write all the data back to the DB
|
|
bytes, err := json.Marshal(peers)
|
|
if err != nil {
|
|
tms.Logger.Error("Failed to encode the TrustHistory", "err", err)
|
|
return
|
|
}
|
|
tms.db.SetSync(trustMetricKey, bytes)
|
|
}
|
|
|
|
// Periodically saves the trust history data to the DB
|
|
func (tms *TrustMetricStore) saveRoutine() {
|
|
t := time.NewTicker(defaultStorePeriodicSaveInterval)
|
|
defer t.Stop()
|
|
loop:
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
tms.SaveToDB()
|
|
case <-tms.Quit:
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
|
|
const (
|
|
// The weight applied to the derivative when current behavior is >= previous behavior
|
|
defaultDerivativeGamma1 = 0
|
|
|
|
// The weight applied to the derivative when current behavior is less than previous behavior
|
|
defaultDerivativeGamma2 = 1.0
|
|
|
|
// The weight applied to history data values when calculating the history value
|
|
defaultHistoryDataWeight = 0.8
|
|
)
|
|
|
|
// TrustMetric - keeps track of peer reliability
|
|
// See tendermint/docs/architecture/adr-006-trust-metric.md for details
|
|
type TrustMetric struct {
|
|
// Mutex that protects the metric from concurrent access
|
|
mtx sync.Mutex
|
|
|
|
// Determines the percentage given to current behavior
|
|
proportionalWeight float64
|
|
|
|
// Determines the percentage given to prior behavior
|
|
integralWeight float64
|
|
|
|
// Count of how many time intervals this metric has been tracking
|
|
numIntervals int
|
|
|
|
// Size of the time interval window for this trust metric
|
|
maxIntervals int
|
|
|
|
// The time duration for a single time interval
|
|
intervalLen time.Duration
|
|
|
|
// Stores the trust history data for this metric
|
|
history []float64
|
|
|
|
// Weights applied to the history data when calculating the history value
|
|
historyWeights []float64
|
|
|
|
// The sum of the history weights used when calculating the history value
|
|
historyWeightSum float64
|
|
|
|
// The current number of history data elements
|
|
historySize int
|
|
|
|
// The maximum number of history data elements
|
|
historyMaxSize int
|
|
|
|
// The calculated history value for the current time interval
|
|
historyValue float64
|
|
|
|
// The number of recorded good and bad events for the current time interval
|
|
bad, good float64
|
|
|
|
// While true, history data is not modified
|
|
paused bool
|
|
|
|
// Signal channel for stopping the trust metric go-routine
|
|
stop chan struct{}
|
|
}
|
|
|
|
// MetricHistoryJSON - history data necessary to save the trust metric
|
|
type MetricHistoryJSON struct {
|
|
NumIntervals int `json:"intervals"`
|
|
History []float64 `json:"history"`
|
|
}
|
|
|
|
// Returns a snapshot of the trust metric history data
|
|
func (tm *TrustMetric) HistoryJSON() MetricHistoryJSON {
|
|
tm.mtx.Lock()
|
|
defer tm.mtx.Unlock()
|
|
|
|
return MetricHistoryJSON{
|
|
NumIntervals: tm.numIntervals,
|
|
History: tm.history,
|
|
}
|
|
}
|
|
|
|
// Instantiates a trust metric by loading the history data for a single peer.
|
|
// This is called only once and only right after creation, which is why the
|
|
// lock is not held while accessing the trust metric struct members
|
|
func (tm *TrustMetric) Init(hist MetricHistoryJSON) {
|
|
// Restore the number of time intervals we have previously tracked
|
|
if hist.NumIntervals > tm.maxIntervals {
|
|
hist.NumIntervals = tm.maxIntervals
|
|
}
|
|
tm.numIntervals = hist.NumIntervals
|
|
// Restore the history and its current size
|
|
if len(hist.History) > tm.historyMaxSize {
|
|
// Keep the history no larger than historyMaxSize
|
|
last := len(hist.History) - tm.historyMaxSize
|
|
hist.History = hist.History[last:]
|
|
}
|
|
tm.history = hist.History
|
|
tm.historySize = len(tm.history)
|
|
// Create the history weight values and weight sum
|
|
for i := 1; i <= tm.numIntervals; i++ {
|
|
x := math.Pow(defaultHistoryDataWeight, float64(i)) // Optimistic weight
|
|
tm.historyWeights = append(tm.historyWeights, x)
|
|
}
|
|
|
|
for _, v := range tm.historyWeights {
|
|
tm.historyWeightSum += v
|
|
}
|
|
// Calculate the history value based on the loaded history data
|
|
tm.historyValue = tm.calcHistoryValue()
|
|
}
|
|
|
|
// Pause tells the metric to pause recording data over time intervals.
|
|
// All method calls that indicate events will unpause the metric
|
|
func (tm *TrustMetric) Pause() {
|
|
tm.mtx.Lock()
|
|
defer tm.mtx.Unlock()
|
|
|
|
// Pause the metric for now
|
|
tm.paused = true
|
|
}
|
|
|
|
// Stop tells the metric to stop recording data over time intervals
|
|
func (tm *TrustMetric) Stop() {
|
|
tm.stop <- struct{}{}
|
|
}
|
|
|
|
// BadEvents indicates that an undesirable event(s) took place
|
|
func (tm *TrustMetric) BadEvents(num int) {
|
|
tm.mtx.Lock()
|
|
defer tm.mtx.Unlock()
|
|
|
|
tm.unpause()
|
|
tm.bad += float64(num)
|
|
}
|
|
|
|
// GoodEvents indicates that a desirable event(s) took place
|
|
func (tm *TrustMetric) GoodEvents(num int) {
|
|
tm.mtx.Lock()
|
|
defer tm.mtx.Unlock()
|
|
|
|
tm.unpause()
|
|
tm.good += float64(num)
|
|
}
|
|
|
|
// TrustValue gets the dependable trust value; always between 0 and 1
|
|
func (tm *TrustMetric) TrustValue() float64 {
|
|
tm.mtx.Lock()
|
|
defer tm.mtx.Unlock()
|
|
|
|
return tm.calcTrustValue()
|
|
}
|
|
|
|
// TrustScore gets a score based on the trust value always between 0 and 100
|
|
func (tm *TrustMetric) TrustScore() int {
|
|
score := tm.TrustValue() * 100
|
|
|
|
return int(math.Floor(score))
|
|
}
|
|
|
|
// NextTimeInterval saves current time interval data and prepares for the following interval
|
|
func (tm *TrustMetric) NextTimeInterval() {
|
|
tm.mtx.Lock()
|
|
defer tm.mtx.Unlock()
|
|
|
|
if tm.paused {
|
|
// Do not prepare for the next time interval while paused
|
|
return
|
|
}
|
|
|
|
// Add the current trust value to the history data
|
|
newHist := tm.calcTrustValue()
|
|
tm.history = append(tm.history, newHist)
|
|
|
|
// Update history and interval counters
|
|
if tm.historySize < tm.historyMaxSize {
|
|
tm.historySize++
|
|
} else {
|
|
// Keep the history no larger than historyMaxSize
|
|
last := len(tm.history) - tm.historyMaxSize
|
|
tm.history = tm.history[last:]
|
|
}
|
|
|
|
if tm.numIntervals < tm.maxIntervals {
|
|
tm.numIntervals++
|
|
// Add the optimistic weight for the new time interval
|
|
wk := math.Pow(defaultHistoryDataWeight, float64(tm.numIntervals))
|
|
tm.historyWeights = append(tm.historyWeights, wk)
|
|
tm.historyWeightSum += wk
|
|
}
|
|
|
|
// Update the history data using Faded Memories
|
|
tm.updateFadedMemory()
|
|
// Calculate the history value for the upcoming time interval
|
|
tm.historyValue = tm.calcHistoryValue()
|
|
tm.good = 0
|
|
tm.bad = 0
|
|
}
|
|
|
|
// Copy returns a new trust metric with members containing the same values
|
|
func (tm *TrustMetric) Copy() *TrustMetric {
|
|
if tm == nil {
|
|
return nil
|
|
}
|
|
|
|
return &TrustMetric{
|
|
proportionalWeight: tm.proportionalWeight,
|
|
integralWeight: tm.integralWeight,
|
|
numIntervals: tm.numIntervals,
|
|
maxIntervals: tm.maxIntervals,
|
|
intervalLen: tm.intervalLen,
|
|
history: tm.history,
|
|
historyWeights: tm.historyWeights,
|
|
historyWeightSum: tm.historyWeightSum,
|
|
historySize: tm.historySize,
|
|
historyMaxSize: tm.historyMaxSize,
|
|
historyValue: tm.historyValue,
|
|
good: tm.good,
|
|
bad: tm.bad,
|
|
paused: tm.paused,
|
|
stop: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// TrustMetricConfig - Configures the weight functions and time intervals for the metric
|
|
type TrustMetricConfig struct {
|
|
// Determines the percentage given to current behavior
|
|
ProportionalWeight float64
|
|
|
|
// Determines the percentage given to prior behavior
|
|
IntegralWeight float64
|
|
|
|
// The window of time that the trust metric will track events across.
|
|
// This can be set to cover many days without issue
|
|
TrackingWindow time.Duration
|
|
|
|
// Each interval should be short for adapability.
|
|
// Less than 30 seconds is too sensitive,
|
|
// and greater than 5 minutes will make the metric numb
|
|
IntervalLength time.Duration
|
|
}
|
|
|
|
// DefaultConfig returns a config with values that have been tested and produce desirable results
|
|
func DefaultConfig() TrustMetricConfig {
|
|
return TrustMetricConfig{
|
|
ProportionalWeight: 0.4,
|
|
IntegralWeight: 0.6,
|
|
TrackingWindow: (time.Minute * 60 * 24) * 14, // 14 days.
|
|
IntervalLength: 1 * time.Minute,
|
|
}
|
|
}
|
|
|
|
// NewMetric returns a trust metric with the default configuration
|
|
func NewMetric() *TrustMetric {
|
|
return NewMetricWithConfig(DefaultConfig())
|
|
}
|
|
|
|
// NewMetricWithConfig returns a trust metric with a custom configuration
|
|
func NewMetricWithConfig(tmc TrustMetricConfig) *TrustMetric {
|
|
tm := new(TrustMetric)
|
|
config := customConfig(tmc)
|
|
|
|
// Setup using the configuration values
|
|
tm.proportionalWeight = config.ProportionalWeight
|
|
tm.integralWeight = config.IntegralWeight
|
|
tm.intervalLen = config.IntervalLength
|
|
// The maximum number of time intervals is the tracking window / interval length
|
|
tm.maxIntervals = int(config.TrackingWindow / tm.intervalLen)
|
|
// The history size will be determined by the maximum number of time intervals
|
|
tm.historyMaxSize = intervalToHistoryOffset(tm.maxIntervals) + 1
|
|
// This metric has a perfect history so far
|
|
tm.historyValue = 1.0
|
|
// Setup the stop channel
|
|
tm.stop = make(chan struct{})
|
|
|
|
go tm.processRequests()
|
|
return tm
|
|
}
|
|
|
|
/* Private methods */
|
|
|
|
// Ensures that all configuration elements have valid values
|
|
func customConfig(tmc TrustMetricConfig) TrustMetricConfig {
|
|
config := DefaultConfig()
|
|
|
|
// Check the config for set values, and setup appropriately
|
|
if tmc.ProportionalWeight > 0 {
|
|
config.ProportionalWeight = tmc.ProportionalWeight
|
|
}
|
|
|
|
if tmc.IntegralWeight > 0 {
|
|
config.IntegralWeight = tmc.IntegralWeight
|
|
}
|
|
|
|
if tmc.IntervalLength > time.Duration(0) {
|
|
config.IntervalLength = tmc.IntervalLength
|
|
}
|
|
|
|
if tmc.TrackingWindow > time.Duration(0) &&
|
|
tmc.TrackingWindow >= config.IntervalLength {
|
|
config.TrackingWindow = tmc.TrackingWindow
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// Wakes the trust metric up if it is currently paused
|
|
// This method needs to be called with the mutex locked
|
|
func (tm *TrustMetric) unpause() {
|
|
// Check if this is the first experience with
|
|
// what we are tracking since being paused
|
|
if tm.paused {
|
|
tm.good = 0
|
|
tm.bad = 0
|
|
// New events cause us to unpause the metric
|
|
tm.paused = false
|
|
}
|
|
}
|
|
|
|
// Calculates the derivative component
|
|
func (tm *TrustMetric) derivativeValue() float64 {
|
|
return tm.proportionalValue() - tm.historyValue
|
|
}
|
|
|
|
// Strengthens the derivative component when the change is negative
|
|
func (tm *TrustMetric) weightedDerivative() float64 {
|
|
var weight float64 = defaultDerivativeGamma1
|
|
|
|
d := tm.derivativeValue()
|
|
if d < 0 {
|
|
weight = defaultDerivativeGamma2
|
|
}
|
|
return weight * d
|
|
}
|
|
|
|
// Performs the update for our Faded Memories process, which allows the
|
|
// trust metric tracking window to be large while maintaining a small
|
|
// number of history data values
|
|
func (tm *TrustMetric) updateFadedMemory() {
|
|
if tm.historySize < 2 {
|
|
return
|
|
}
|
|
|
|
end := tm.historySize - 1
|
|
// Keep the most recent history element
|
|
for count := 1; count < tm.historySize; count++ {
|
|
i := end - count
|
|
// The older the data is, the more we spread it out
|
|
x := math.Pow(2, float64(count))
|
|
// Two history data values are merged into a single value
|
|
tm.history[i] = ((tm.history[i] * (x - 1)) + tm.history[i+1]) / x
|
|
}
|
|
}
|
|
|
|
// Map the interval value down to an offset from the beginning of history
|
|
func intervalToHistoryOffset(interval int) int {
|
|
// The system maintains 2^m interval values in the form of m history
|
|
// data values. Therefore, we access the ith interval by obtaining
|
|
// the history data index = the floor of log2(i)
|
|
return int(math.Floor(math.Log2(float64(interval))))
|
|
}
|
|
|
|
// Retrieves the actual history data value that represents the requested time interval
|
|
func (tm *TrustMetric) fadedMemoryValue(interval int) float64 {
|
|
first := tm.historySize - 1
|
|
|
|
if interval == 0 {
|
|
// Base case
|
|
return tm.history[first]
|
|
}
|
|
|
|
offset := intervalToHistoryOffset(interval)
|
|
return tm.history[first-offset]
|
|
}
|
|
|
|
// Calculates the integral (history) component of the trust value
|
|
func (tm *TrustMetric) calcHistoryValue() float64 {
|
|
var hv float64
|
|
|
|
for i := 0; i < tm.numIntervals; i++ {
|
|
hv += tm.fadedMemoryValue(i) * tm.historyWeights[i]
|
|
}
|
|
|
|
return hv / tm.historyWeightSum
|
|
}
|
|
|
|
// Calculates the current score for good/bad experiences
|
|
func (tm *TrustMetric) proportionalValue() float64 {
|
|
value := 1.0
|
|
|
|
total := tm.good + tm.bad
|
|
if total > 0 {
|
|
value = tm.good / total
|
|
}
|
|
return value
|
|
}
|
|
|
|
// Calculates the trust value for the request processing
|
|
func (tm *TrustMetric) calcTrustValue() float64 {
|
|
weightedP := tm.proportionalWeight * tm.proportionalValue()
|
|
weightedI := tm.integralWeight * tm.historyValue
|
|
weightedD := tm.weightedDerivative()
|
|
|
|
tv := weightedP + weightedI + weightedD
|
|
// Do not return a negative value.
|
|
if tv < 0 {
|
|
tv = 0
|
|
}
|
|
return tv
|
|
}
|
|
|
|
// This method is for a goroutine that handles all requests on the metric
|
|
func (tm *TrustMetric) processRequests() {
|
|
t := time.NewTicker(tm.intervalLen)
|
|
defer t.Stop()
|
|
loop:
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
tm.NextTimeInterval()
|
|
case <-tm.stop:
|
|
// Stop all further tracking for this metric
|
|
break loop
|
|
}
|
|
}
|
|
}
|