From e160a6198cc687c4b23f31cf2828d529a7a94b2d Mon Sep 17 00:00:00 2001 From: caffix Date: Tue, 24 Oct 2017 20:28:20 -0400 Subject: [PATCH 1/5] added initial trust metric design doc and code --- docs/architecture/adr-006-trust-metric.md | 117 +++++++ docs/architecture/img/formula1.png | Bin 0 -> 9833 bytes docs/architecture/img/formula2.png | Bin 0 -> 5942 bytes p2p/trust/trustmetric.go | 394 ++++++++++++++++++++++ 4 files changed, 511 insertions(+) create mode 100644 docs/architecture/adr-006-trust-metric.md create mode 100644 docs/architecture/img/formula1.png create mode 100644 docs/architecture/img/formula2.png create mode 100644 p2p/trust/trustmetric.go diff --git a/docs/architecture/adr-006-trust-metric.md b/docs/architecture/adr-006-trust-metric.md new file mode 100644 index 000000000..29861ce65 --- /dev/null +++ b/docs/architecture/adr-006-trust-metric.md @@ -0,0 +1,117 @@ +# Trust Metric Design + +## Overview + +The proposed trust metric will allow Tendermint to maintain local trust rankings for peers it has directly interacted with, which can then be used to implement soft security controls. The calculations were obtained from the [TrustGuard](https://dl.acm.org/citation.cfm?id=1060808) project. + +## Background + +The Tendermint Core project developers would like to improve Tendermint security and reliability by keeping track of the level of trustworthiness peers have demonstrated within the peer-to-peer network. This way, undesirable outcomes from peers will not immediately result in them being dropped from the network (potentially causing drastic changes to take place). Instead, peers behavior can be monitored with appropriate metrics and be removed from the network once Tendermint Core is certain the peer is a threat. For example, when the PEXReactor makes a request for peers network addresses from a already known peer, and the returned network addresses are unreachable, this untrustworthy behavior should be tracked. Returning a few bad network addresses probably shouldn’t cause a peer to be dropped, while excessive amounts of this behavior does qualify the peer being dropped. + +Trust metrics can be circumvented by malicious nodes through the use of strategic oscillation techniques, which adapts the malicious node’s behavior pattern in order to maximize its goals. For instance, if the malicious node learns that the time interval of the Tendermint trust metric is *X* hours, then it could wait *X* hours in-between malicious activities. We could try to combat this issue by increasing the interval length, yet this will make the system less adaptive to recent events. + +Instead, having shorter intervals, but keeping a history of interval values, will give our metric the flexibility needed in order to keep the network stable, while also making it resilient against a strategic malicious node in the Tendermint peer-to-peer network. Also, the metric can access trust data over a rather long period of time while not greatly increasing its history size by aggregating older history values over a larger number of intervals, and at the same time, maintain great precision for the recent intervals. This approach is referred to as fading memories, and closely resembles the way human beings remember their experiences. The trade-off to using history data is that the interval values should be preserved in-between executions of the node. + +## Scope + +The proposed trust metric will be implemented as a Go programming language object that will allow a developer to inform the object of all good and bad events relevant to the trust object instantiation, and at any time, the metric can be queried for the current trust ranking. Methods will be provided for storing trust metric history data that is required across instantiations. + +## Detailed Design + +This section will cover the process being considered for calculating the trust ranking and the interface for the trust metric. + +### Proposed Process + +The proposed trust metric will count good and bad events relevant to the object, and calculate the percent of counters that are good over an interval with a predefined duration. This is the procedure that will continue for the life of the trust metric. When the trust metric is queried for the current **trust value**, a resilient equation will be utilized to perform the calculation. + +The equation being proposed resembles a Proportional-Integral-Derivative (PID) controller used in control systems. The proportional component allows us to be sensitive to the value of the most recent interval, while the integral component allows us to incorporate trust values stored in the history data, and the derivative component allows us to give weight to sudden changes in the behavior of a peer. We compute the trust value of a peer in interval i based on its current trust ranking, its trust rating history prior to interval *i* (over the past *maxH* number of intervals) and its trust ranking fluctuation. We will break up the equation into the three components. + +```math +(1) Proportional Value = a * R[i] +``` + +where *R*[*i*] denotes the raw trust value at time interval *i* (where *i* == 0 being current time) and *a* is the weight applied to the contribution of the current reports. The next component of our equation uses a weighted sum over the last *maxH* intervals to calculate the history value for time *i*: + + +`H[i] = ` ![formula1](https://github.com/tendermint/tendermint/blob/develop/docs/architecture/img/formula1.png "Weighted Sum Formula") + + +The weights can be chosen either optimistically or pessimistically. With the history value available, we can now finish calculating the integral value: + +```math +(2) Integral Value = b * H[i] +``` + +Where *H*[*i*] denotes the history value at time interval *i* and *b* is the weight applied to the contribution of past performance for the object being measured. The derivative component will be calculated as follows: + +```math +D[i] = R[i] – H[i] + +(3) Derivative Value = (c * D[i]) * D[i] +``` + +Where the value of *c* is selected based on the *D*[*i*] value relative to zero. With the three components brought together, our trust value equation is calculated as follows: + +```math +TrustValue[i] = a * R[i] + b * H[i] + (c * D[i]) * D[i] +``` + +As a performance optimization that will keep the amount of raw interval data being saved to a reasonable size of *m*, while allowing us to represent 2^*m* - 1 history intervals, we can employ the fading memories technique that will trade space and time complexity for the precision of the history data values by summarizing larger quantities of less recent values. While our equation above attempts to access up to *maxH* (which can be 2^*m* - 1), we will map those requests down to *m* values using equation 4 below: + +```math +(4) j = index, where index > 0 +``` + +Where *j* is one of *(0, 1, 2, … , m – 1)* indices used to access history interval data. Now we can access the raw intervals using the following calculations: + +```math +R[0] = raw data for current time interval +``` + +`R[j] = ` ![formula2](https://github.com/tendermint/tendermint/blob/develop/docs/architecture/img/formula2.png "Fading Memories Formula") + + +### Interface Detailed Design + +This section will cover the Go programming language API designed for the previously proposed process. Below is the interface for a TrustMetric: + +```go +package trust + +type TrustMetric struct { + +} + +type TrustMetricConfig struct { + ProportionalWeight float64 + IntegralWeight float64 + HistoryMaxSize int + IntervalLen time.Duration +} + +func (tm *TrustMetric) Stop() + +func (tm *TrustMetric) IncBad() + +func (tm *TrustMetric) AddBad(num int) + +func (tm *TrustMetric) IncGood() + +func (tm *TrustMetric) AddGood(num int) + +// get the dependable trust value +func (tm *TrustMetric) TrustValue() float64 + +func NewMetric() *TrustMetric + +func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric + +func GetPeerTrustMetric(key string) *TrustMetric + +func PeerDisconnected(key string) + +``` + +## References + +S. Mudhakar, L. Xiong, and L. Liu, “TrustGuard: Countering Vulnerabilities in Reputation Management for Decentralized Overlay Networks,” in *Proceedings of the 14th international conference on World Wide Web, pp. 422-431*, May 2005. \ No newline at end of file diff --git a/docs/architecture/img/formula1.png b/docs/architecture/img/formula1.png new file mode 100644 index 0000000000000000000000000000000000000000..447ee30f5734f1b562a26664f16025cd69265b97 GIT binary patch literal 9833 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 pKwu+9vx2t#M;`x6DG$FvU4vE19d12Dwb)i}B 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 TrGQ6-8x16{CRZh6@!@{}Sksjb literal 0 HcmV?d00001 diff --git a/p2p/trust/trustmetric.go b/p2p/trust/trustmetric.go new file mode 100644 index 000000000..a1257f4b9 --- /dev/null +++ b/p2p/trust/trustmetric.go @@ -0,0 +1,394 @@ +package trust + +import ( + "encoding/json" + "io/ioutil" + "math" + "os" + "path/filepath" + "time" +) + +var ( + store *trustMetricStore +) + +type peerMetricRequest struct { + Key string + Resp chan *TrustMetric +} + +type trustMetricStore struct { + PeerMetrics map[string]*TrustMetric + Requests chan *peerMetricRequest + Disconn chan string +} + +func init() { + store = &trustMetricStore{ + PeerMetrics: make(map[string]*TrustMetric), + Requests: make(chan *peerMetricRequest, 10), + Disconn: make(chan string, 10), + } + + go store.processRequests() +} + +type peerHistory struct { + NumIntervals int `json:"intervals"` + History []float64 `json:"history"` +} + +func loadSaveFromFile(key string, isLoad bool, data *peerHistory) *peerHistory { + tmhome, ok := os.LookupEnv("TMHOME") + if !ok { + return nil + } + + filename := filepath.Join(tmhome, "trust_history.json") + + peers := make(map[string]peerHistory, 0) + // read in previously written history data + content, err := ioutil.ReadFile(filename) + if err == nil { + err = json.Unmarshal(content, &peers) + } + + var result *peerHistory + + if isLoad { + if p, ok := peers[key]; ok { + result = &p + } + } else { + peers[key] = *data + + b, err := json.Marshal(peers) + if err == nil { + err = ioutil.WriteFile(filename, b, 0644) + } + } + return result +} + +func createLoadPeerMetric(key string) *TrustMetric { + tm := NewMetric() + + if tm == nil { + return tm + } + + // attempt to load the peer's trust history data + if ph := loadSaveFromFile(key, true, nil); ph != nil { + tm.historySize = len(ph.History) + + if tm.historySize > 0 { + tm.numIntervals = ph.NumIntervals + tm.history = ph.History + + tm.historyValue = tm.calcHistoryValue() + } + } + return tm +} + +func (tms *trustMetricStore) processRequests() { + for { + select { + case req := <-tms.Requests: + tm, ok := tms.PeerMetrics[req.Key] + + if !ok { + tm = createLoadPeerMetric(req.Key) + + if tm != nil { + tms.PeerMetrics[req.Key] = tm + } + } + + req.Resp <- tm + case key := <-tms.Disconn: + if tm, ok := tms.PeerMetrics[key]; ok { + ph := peerHistory{ + NumIntervals: tm.numIntervals, + History: tm.history, + } + + tm.Stop() + delete(tms.PeerMetrics, key) + loadSaveFromFile(key, false, &ph) + } + } + } +} + +// request a TrustMetric by Peer Key +func GetPeerTrustMetric(key string) *TrustMetric { + resp := make(chan *TrustMetric, 1) + + store.Requests <- &peerMetricRequest{Key: key, Resp: resp} + return <-resp +} + +// the trust metric store should know when a Peer disconnects +func PeerDisconnected(key string) { + store.Disconn <- key +} + +// keep track of Peer reliability +type TrustMetric struct { + proportionalWeight float64 + integralWeight float64 + numIntervals int + maxIntervals int + intervalLen time.Duration + history []float64 + historySize int + historyMaxSize int + historyValue float64 + bad, good float64 + stop chan int + update chan *updateBadGood + trustValue chan *reqTrustValue +} + +type TrustMetricConfig struct { + // be careful changing these weights + ProportionalWeight float64 + IntegralWeight float64 + // don't allow 2^HistoryMaxSize to be greater than int max value + HistoryMaxSize int + // each interval should be short for adapability + // less than 30 seconds is too sensitive, + // and greater than 5 minutes will make the metric numb + IntervalLen time.Duration +} + +func defaultConfig() *TrustMetricConfig { + return &TrustMetricConfig{ + ProportionalWeight: 0.4, + IntegralWeight: 0.6, + HistoryMaxSize: 16, + IntervalLen: 1 * time.Minute, + } +} + +type updateBadGood struct { + IsBad bool + Add int +} + +type reqTrustValue struct { + Resp chan float64 +} + +// calculates the derivative component +func (tm *TrustMetric) derivativeValue() float64 { + return tm.proportionalValue() - tm.historyValue +} + +// strengthens the derivative component +func (tm *TrustMetric) weightedDerivative() float64 { + var weight float64 + + d := tm.derivativeValue() + if d < 0 { + weight = 1.0 + } + + return weight * d +} + +func (tm *TrustMetric) fadedMemoryValue(interval int) float64 { + if interval == 0 { + // base case + return tm.history[0] + } + + index := int(math.Floor(math.Log(float64(interval)) / math.Log(2))) + // map the interval value down to an actual history index + return tm.history[index] +} + +func (tm *TrustMetric) updateFadedMemory() { + if tm.historySize < 2 { + return + } + + // keep the last history element + faded := tm.history[:1] + + for i := 1; i < tm.historySize; i++ { + x := math.Pow(2, float64(i)) + + ftv := ((tm.history[i] * (x - 1)) + tm.history[i-1]) / x + + faded = append(faded, ftv) + } + + tm.history = faded +} + +// calculates the integral (history) component of the trust value +func (tm *TrustMetric) calcHistoryValue() float64 { + var wk []float64 + + // create the weights + hlen := tm.numIntervals + for i := 0; i < hlen; i++ { + x := math.Pow(.8, float64(i+1)) // optimistic wk + wk = append(wk, x) + } + + var wsum float64 + // calculate the sum of the weights + for _, v := range wk { + wsum += v + } + + var hv float64 + // calculate the history value + for i := 0; i < hlen; i++ { + weight := wk[i] / wsum + hv += tm.fadedMemoryValue(i) * weight + } + return hv +} + +// calculates the current score for good experiences +func (tm *TrustMetric) proportionalValue() float64 { + value := 1.0 + // bad events are worth more + total := tm.good + math.Pow(tm.bad, 2) + + if tm.bad > 0 || tm.good > 0 { + value = tm.good / total + } + return value +} + +func (tm *TrustMetric) calcTrustValue() float64 { + weightedP := tm.proportionalWeight * tm.proportionalValue() + weightedI := tm.integralWeight * tm.historyValue + weightedD := tm.weightedDerivative() + + tv := weightedP + weightedI + weightedD + if tv < 0 { + tv = 0 + } + return tv +} + +func (tm *TrustMetric) processRequests() { + t := time.NewTicker(tm.intervalLen) + defer t.Stop() +loop: + for { + select { + case bg := <-tm.update: + if bg.IsBad { + tm.bad += float64(bg.Add) + } else { + tm.good += float64(bg.Add) + } + case rtv := <-tm.trustValue: + // send the calculated trust value back + rtv.Resp <- tm.calcTrustValue() + case <-t.C: + newHist := tm.calcTrustValue() + tm.history = append([]float64{newHist}, tm.history...) + + if tm.historySize < tm.historyMaxSize { + tm.historySize++ + } else { + tm.history = tm.history[:tm.historyMaxSize] + } + + if tm.numIntervals < tm.maxIntervals { + tm.numIntervals++ + } + + tm.updateFadedMemory() + tm.historyValue = tm.calcHistoryValue() + tm.good = 0 + tm.bad = 0 + case <-tm.stop: + break loop + } + } +} + +func (tm *TrustMetric) Stop() { + tm.stop <- 1 +} + +// indicate that an undesirable event took place +func (tm *TrustMetric) IncBad() { + tm.update <- &updateBadGood{IsBad: true, Add: 1} +} + +// multiple undesirable events need to be acknowledged +func (tm *TrustMetric) AddBad(num int) { + tm.update <- &updateBadGood{IsBad: true, Add: num} +} + +// positive events need to be recorded as well +func (tm *TrustMetric) IncGood() { + tm.update <- &updateBadGood{IsBad: false, Add: 1} +} + +// multiple positive can be indicated in a single call +func (tm *TrustMetric) AddGood(num int) { + tm.update <- &updateBadGood{IsBad: false, Add: num} +} + +// get the dependable trust value; a score that takes a long history into account +func (tm *TrustMetric) TrustValue() float64 { + resp := make(chan float64, 1) + + tm.trustValue <- &reqTrustValue{Resp: resp} + return <-resp +} + +func NewMetric() *TrustMetric { + return NewMetricWithConfig(defaultConfig()) +} + +func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric { + tm := new(TrustMetric) + dc := defaultConfig() + + if tmc.ProportionalWeight != 0 { + tm.proportionalWeight = tmc.ProportionalWeight + } else { + tm.proportionalWeight = dc.ProportionalWeight + } + + if tmc.IntegralWeight != 0 { + tm.integralWeight = tmc.IntegralWeight + } else { + tm.integralWeight = dc.IntegralWeight + } + + if tmc.HistoryMaxSize != 0 { + tm.historyMaxSize = tmc.HistoryMaxSize + } else { + tm.historyMaxSize = dc.HistoryMaxSize + } + + if tmc.IntervalLen != time.Duration(0) { + tm.intervalLen = tmc.IntervalLen + } else { + tm.intervalLen = dc.IntervalLen + } + + // this gives our metric a tracking window of days + tm.maxIntervals = int(math.Pow(2, float64(tm.historyMaxSize))) + tm.historyValue = 1.0 + tm.update = make(chan *updateBadGood, 10) + tm.trustValue = make(chan *reqTrustValue, 10) + tm.stop = make(chan int, 1) + + go tm.processRequests() + return tm +} From 54c25ccbf51d3503550c553da8b081b0f1d4a16a Mon Sep 17 00:00:00 2001 From: caffix Date: Mon, 30 Oct 2017 15:34:49 -0400 Subject: [PATCH 2/5] integrated trust metric store as per PR comments --- config/config.go | 11 +- docs/architecture/adr-006-trust-metric.md | 80 ++- node/node.go | 12 +- p2p/trust/trustmetric.go | 570 ++++++++++++++-------- 4 files changed, 444 insertions(+), 229 deletions(-) diff --git a/config/config.go b/config/config.go index 23da4f405..46fb55ec6 100644 --- a/config/config.go +++ b/config/config.go @@ -214,6 +214,9 @@ type P2PConfig struct { // Set true for strict address routability rules AddrBookStrict bool `mapstructure:"addr_book_strict"` + // Path to the trust history file + TrustHistory string `mapstructure:"trust_history_file"` + // Set true to enable the peer-exchange reactor PexReactor bool `mapstructure:"pex"` @@ -239,6 +242,7 @@ func DefaultP2PConfig() *P2PConfig { ListenAddress: "tcp://0.0.0.0:46656", AddrBook: "addrbook.json", AddrBookStrict: true, + TrustHistory: "trusthistory.json", MaxNumPeers: 50, FlushThrottleTimeout: 100, MaxMsgPacketPayloadSize: 1024, // 1 kB @@ -255,11 +259,16 @@ func TestP2PConfig() *P2PConfig { return conf } -// AddrBookFile returns the full path to the address bool +// AddrBookFile returns the full path to the address book func (p *P2PConfig) AddrBookFile() string { return rootify(p.AddrBook, p.RootDir) } +// TrustHistoryFile returns the full path to the trust metric store history +func (p *P2PConfig) TrustHistoryFile() string { + return rootify(p.TrustHistory, p.RootDir) +} + //----------------------------------------------------------------------------- // MempoolConfig diff --git a/docs/architecture/adr-006-trust-metric.md b/docs/architecture/adr-006-trust-metric.md index 29861ce65..6fc5f9ea6 100644 --- a/docs/architecture/adr-006-trust-metric.md +++ b/docs/architecture/adr-006-trust-metric.md @@ -76,39 +76,91 @@ R[0] = raw data for current time interval This section will cover the Go programming language API designed for the previously proposed process. Below is the interface for a TrustMetric: ```go + package trust -type TrustMetric struct { + +// TrustMetricStore - Manages all trust metrics for peers +type TrustMetricStore struct { + cmn.BaseService + // Private elements } -type TrustMetricConfig struct { - ProportionalWeight float64 - IntegralWeight float64 - HistoryMaxSize int - IntervalLen time.Duration +// OnStart implements Service +func (tms *TrustMetricStore) OnStart() error + +/ OnStop implements Service +func (tms *TrustMetricStore) OnStop() + +// NewTrustMetricStore returns a store that optionally saves data to +// the file path and uses the optional config when creating new trust metrics +func NewTrustMetricStore(filePath string, tmc *TrustMetricConfig) *TrustMetricStore + +// GetPeerTrustMetric returns a trust metric by peer key +func (tms *TrustMetricStore) GetPeerTrustMetric(key string) *TrustMetric + +// PeerDisconnected pauses the trust metric associated with the peer identified by the key +func (tms *TrustMetricStore) PeerDisconnected(key string) + + +//---------------------------------------------------------------------------------------- +// TrustMetric - keeps track of peer reliability +type TrustMetric struct { + // Private elements. } +// Pause tells the metric to pause recording data over time intervals +func (tm *TrustMetric) Pause() + +// Stop tells the metric to stop recording data over time intervals func (tm *TrustMetric) Stop() -func (tm *TrustMetric) IncBad() +// BadEvent indicates that an undesirable event took place +func (tm *TrustMetric) BadEvent() -func (tm *TrustMetric) AddBad(num int) +// AddBadEvents acknowledges multiple undesirable events +func (tm *TrustMetric) AddBadEvents(num int) -func (tm *TrustMetric) IncGood() +// GoodEvent indicates that a desirable event took place +func (tm *TrustMetric) GoodEvent() -func (tm *TrustMetric) AddGood(num int) +// AddGoodEvents acknowledges multiple desirable events +func (tm *TrustMetric) AddGoodEvents(num int) -// get the dependable trust value +// TrustValue gets the dependable trust value; always between 0 and 1 func (tm *TrustMetric) TrustValue() float64 +// TrustScore gets a score based on the trust value always between 0 and 100 +func (tm *TrustMetric) TrustScore() int + +// NewMetric returns a trust metric with the default configuration func NewMetric() *TrustMetric -func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric -func GetPeerTrustMetric(key string) *TrustMetric +// 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 -func PeerDisconnected(key string) + // 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 + IntervalLen time.Duration +} + +// DefaultConfig returns a config with values that have been tested and produce desirable results +func DefaultConfig() *TrustMetricConfig + +// NewMetricWithConfig returns a trust metric with a custom configuration +func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric ``` diff --git a/node/node.go b/node/node.go index c8029cf81..29be71caf 100644 --- a/node/node.go +++ b/node/node.go @@ -22,6 +22,7 @@ import ( "github.com/tendermint/tendermint/consensus" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/p2p/trust" "github.com/tendermint/tendermint/proxy" rpccore "github.com/tendermint/tendermint/rpc/core" grpccore "github.com/tendermint/tendermint/rpc/grpc" @@ -95,9 +96,10 @@ type Node struct { privValidator types.PrivValidator // local node's validator key // network - privKey crypto.PrivKeyEd25519 // local node's p2p key - sw *p2p.Switch // p2p connections - addrBook *p2p.AddrBook // known peers + privKey crypto.PrivKeyEd25519 // local node's p2p key + sw *p2p.Switch // p2p connections + addrBook *p2p.AddrBook // known peers + tmStore *trust.TrustMetricStore // trust metrics for all peers // services eventBus *types.EventBus // pub/sub for services @@ -239,9 +241,12 @@ func NewNode(config *cfg.Config, // Optionally, start the pex reactor var addrBook *p2p.AddrBook + var tmStore *trust.TrustMetricStore if config.P2P.PexReactor { addrBook = p2p.NewAddrBook(config.P2P.AddrBookFile(), config.P2P.AddrBookStrict) addrBook.SetLogger(p2pLogger.With("book", config.P2P.AddrBookFile())) + tmStore = trust.NewTrustMetricStore(config.P2P.TrustHistoryFile(), nil) + tmStore.SetLogger(p2pLogger.With("trust", config.P2P.TrustHistoryFile())) pexReactor := p2p.NewPEXReactor(addrBook) pexReactor.SetLogger(p2pLogger) sw.AddReactor("PEX", pexReactor) @@ -297,6 +302,7 @@ func NewNode(config *cfg.Config, privKey: privKey, sw: sw, addrBook: addrBook, + tmStore: tmStore, blockStore: blockStore, bcReactor: bcReactor, diff --git a/p2p/trust/trustmetric.go b/p2p/trust/trustmetric.go index a1257f4b9..e4f202bb2 100644 --- a/p2p/trust/trustmetric.go +++ b/p2p/trust/trustmetric.go @@ -1,253 +1,450 @@ +// 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" "io/ioutil" "math" - "os" - "path/filepath" + "sync" "time" -) -var ( - store *trustMetricStore + cmn "github.com/tendermint/tmlibs/common" ) -type peerMetricRequest struct { - Key string - Resp chan *TrustMetric +// 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 file path where peer trust metric history data will be stored + filePath string + + // This configuration will be used when creating new TrustMetrics + config *TrustMetricConfig +} + +// NewTrustMetricStore returns a store that optionally saves data to +// the file path and uses the optional config when creating new trust metrics +func NewTrustMetricStore(filePath string, tmc *TrustMetricConfig) *TrustMetricStore { + tms := &TrustMetricStore{ + peerMetrics: make(map[string]*TrustMetric), + filePath: filePath, + 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.loadFromFile() + return nil } -type trustMetricStore struct { - PeerMetrics map[string]*TrustMetric - Requests chan *peerMetricRequest - Disconn chan string +// OnStop implements Service +func (tms *TrustMetricStore) OnStop() { + tms.mtx.Lock() + defer tms.mtx.Unlock() + + // Stop all trust metric goroutines + for _, tm := range tms.peerMetrics { + tm.Stop() + } + + tms.saveToFile() + tms.BaseService.OnStop() } -func init() { - store = &trustMetricStore{ - PeerMetrics: make(map[string]*TrustMetric), - Requests: make(chan *peerMetricRequest, 10), - Disconn: make(chan string, 10), +// 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) + if tm != nil { + // The metric needs to be in the map + tms.peerMetrics[key] = tm + } } + return tm +} - go store.processRequests() +// 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() + } } -type peerHistory struct { +/* Loading & Saving */ + +type peerHistoryJSON struct { NumIntervals int `json:"intervals"` History []float64 `json:"history"` } -func loadSaveFromFile(key string, isLoad bool, data *peerHistory) *peerHistory { - tmhome, ok := os.LookupEnv("TMHOME") - if !ok { - return nil +// Loads the history data for the Peer identified by key from the store file. +// cmn.Panics if file is corrupt +func (tms *TrustMetricStore) loadFromFile() bool { + // Check that a file has been configured for use + if tms.filePath == "" { + // The trust metric store can operate without the file + return false } - filename := filepath.Join(tmhome, "trust_history.json") - - peers := make(map[string]peerHistory, 0) - // read in previously written history data - content, err := ioutil.ReadFile(filename) - if err == nil { - err = json.Unmarshal(content, &peers) + // Obtain the history data we have so far + content, err := ioutil.ReadFile(tms.filePath) + if err != nil { + cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", tms.filePath, err)) } - var result *peerHistory + peers := make(map[string]peerHistoryJSON, 0) + err = json.Unmarshal(content, &peers) + if err != nil { + cmn.PanicCrisis(cmn.Fmt("Error decoding file %s: %v", tms.filePath, err)) + } - if isLoad { - if p, ok := peers[key]; ok { - result = &p + // If history data exists in the file, + // load it into trust metrics and recalc + for key, p := range peers { + tm := NewMetricWithConfig(tms.config) + if tm == nil { + continue } - } else { - peers[key] = *data - - b, err := json.Marshal(peers) - if err == nil { - err = ioutil.WriteFile(filename, b, 0644) + // Restore the number of time intervals we have previously tracked + if p.NumIntervals > tm.maxIntervals { + p.NumIntervals = tm.maxIntervals + } + tm.numIntervals = p.NumIntervals + // Restore the history and its current size + if len(p.History) > tm.historyMaxSize { + p.History = p.History[:tm.historyMaxSize] } + tm.history = p.History + tm.historySize = len(tm.history) + // Calculate the history value based on the loaded history data + tm.historyValue = tm.calcHistoryValue() + // Load the peer trust metric into the store + tms.peerMetrics[key] = tm } - return result + return true } -func createLoadPeerMetric(key string) *TrustMetric { - tm := NewMetric() - - if tm == nil { - return tm +// Saves the history data for all peers to the store file +func (tms *TrustMetricStore) saveToFile() { + // Check that a file has been configured for use + if tms.filePath == "" { + // The trust metric store can operate without the file + return } - // attempt to load the peer's trust history data - if ph := loadSaveFromFile(key, true, nil); ph != nil { - tm.historySize = len(ph.History) + tms.Logger.Info("Saving TrustHistory to file", "size", len(tms.peerMetrics)) - if tm.historySize > 0 { - tm.numIntervals = ph.NumIntervals - tm.history = ph.History + peers := make(map[string]peerHistoryJSON, 0) - tm.historyValue = tm.calcHistoryValue() + for key, tm := range tms.peerMetrics { + // Add an entry for the peer identified by key + peers[key] = peerHistoryJSON{ + NumIntervals: tm.numIntervals, + History: tm.history, } } - return tm + + // Write all the data back to the file + b, err := json.Marshal(peers) + if err != nil { + tms.Logger.Error("Failed to encode the TrustHistory", "err", err) + return + } + + err = ioutil.WriteFile(tms.filePath, b, 0644) + if err != nil { + tms.Logger.Error("Failed to save TrustHistory to file", "err", err) + } } -func (tms *trustMetricStore) processRequests() { - for { - select { - case req := <-tms.Requests: - tm, ok := tms.PeerMetrics[req.Key] +//--------------------------------------------------------------------------------------- +// TrustMetric - keeps track of peer reliability +// See tendermint/docs/architecture/adr-006-trust-metric.md for details +type TrustMetric struct { + // Determines the percentage given to current behavior + proportionalWeight float64 - if !ok { - tm = createLoadPeerMetric(req.Key) + // Determines the percentage given to prior behavior + integralWeight float64 - if tm != nil { - tms.PeerMetrics[req.Key] = tm - } - } + // Count of how many time intervals this metric has been tracking + numIntervals int - req.Resp <- tm - case key := <-tms.Disconn: - if tm, ok := tms.PeerMetrics[key]; ok { - ph := peerHistory{ - NumIntervals: tm.numIntervals, - History: tm.history, - } - - tm.Stop() - delete(tms.PeerMetrics, key) - loadSaveFromFile(key, false, &ph) - } - } - } + // 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 + + // 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 + + // Sending true on this channel stops tracking, while false pauses tracking + stop chan bool + + // For sending information about new good/bad events to be recorded + update chan *updateBadGood + + // The channel to request a newly calculated trust value + trustValue chan *reqTrustValue +} + +// For the TrustMetric update channel +type updateBadGood struct { + IsBad bool + Add int } -// request a TrustMetric by Peer Key -func GetPeerTrustMetric(key string) *TrustMetric { - resp := make(chan *TrustMetric, 1) +// For the TrustMetric trustValue channel +type reqTrustValue struct { + // The requested trust value is sent back on this channel + Resp chan float64 +} - store.Requests <- &peerMetricRequest{Key: key, Resp: resp} - return <-resp +// Pause tells the metric to pause recording data over time intervals +func (tm *TrustMetric) Pause() { + tm.stop <- false } -// the trust metric store should know when a Peer disconnects -func PeerDisconnected(key string) { - store.Disconn <- key +// Stop tells the metric to stop recording data over time intervals +func (tm *TrustMetric) Stop() { + tm.stop <- true } -// keep track of Peer reliability -type TrustMetric struct { - proportionalWeight float64 - integralWeight float64 - numIntervals int - maxIntervals int - intervalLen time.Duration - history []float64 - historySize int - historyMaxSize int - historyValue float64 - bad, good float64 - stop chan int - update chan *updateBadGood - trustValue chan *reqTrustValue +// BadEvent indicates that an undesirable event took place +func (tm *TrustMetric) BadEvent() { + tm.update <- &updateBadGood{IsBad: true, Add: 1} } +// AddBadEvents acknowledges multiple undesirable events +func (tm *TrustMetric) AddBadEvents(num int) { + tm.update <- &updateBadGood{IsBad: true, Add: num} +} + +// GoodEvent indicates that a desirable event took place +func (tm *TrustMetric) GoodEvent() { + tm.update <- &updateBadGood{IsBad: false, Add: 1} +} + +// AddGoodEvents acknowledges multiple desirable events +func (tm *TrustMetric) AddGoodEvents(num int) { + tm.update <- &updateBadGood{IsBad: false, Add: num} +} + +// TrustValue gets the dependable trust value; always between 0 and 1 +func (tm *TrustMetric) TrustValue() float64 { + resp := make(chan float64, 1) + + tm.trustValue <- &reqTrustValue{Resp: resp} + return <-resp +} + +// TrustScore gets a score based on the trust value always between 0 and 100 +func (tm *TrustMetric) TrustScore() int { + resp := make(chan float64, 1) + + tm.trustValue <- &reqTrustValue{Resp: resp} + return int(math.Floor(<-resp * 100)) +} + +// TrustMetricConfig - Configures the weight functions and time intervals for the metric type TrustMetricConfig struct { - // be careful changing these weights + // Determines the percentage given to current behavior ProportionalWeight float64 - IntegralWeight float64 - // don't allow 2^HistoryMaxSize to be greater than int max value - HistoryMaxSize int - // each interval should be short for adapability - // less than 30 seconds is too sensitive, + + // 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 IntervalLen time.Duration } -func defaultConfig() *TrustMetricConfig { +// 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, - HistoryMaxSize: 16, + TrackingWindow: (time.Minute * 60 * 24) * 14, // 14 days. IntervalLen: 1 * time.Minute, } } -type updateBadGood struct { - IsBad bool - Add int +// NewMetric returns a trust metric with the default configuration +func NewMetric() *TrustMetric { + return NewMetricWithConfig(nil) } -type reqTrustValue struct { - Resp chan float64 +// NewMetricWithConfig returns a trust metric with a custom configuration +func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric { + var config *TrustMetricConfig + + if tmc == nil { + config = DefaultConfig() + } else { + config = customConfig(tmc) + } + + tm := new(TrustMetric) + + // Setup using the configuration values + tm.proportionalWeight = config.ProportionalWeight + tm.integralWeight = config.IntegralWeight + tm.intervalLen = config.IntervalLen + // 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 = intervalToHistoryIndex(tm.maxIntervals) + 1 + // This metric has a perfect history so far + tm.historyValue = 1.0 + // Setup the channels + tm.update = make(chan *updateBadGood, 10) + tm.trustValue = make(chan *reqTrustValue, 10) + tm.stop = make(chan bool, 2) + + go tm.processRequests() + return tm } -// calculates the derivative component +/* 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.TrackingWindow != time.Duration(0) { + config.TrackingWindow = tmc.TrackingWindow + } + + if tmc.IntervalLen != time.Duration(0) { + config.IntervalLen = tmc.IntervalLen + } + return config +} + +// Calculates the derivative component func (tm *TrustMetric) derivativeValue() float64 { return tm.proportionalValue() - tm.historyValue } -// strengthens the derivative component +// Strengthens the derivative component when the change is negative func (tm *TrustMetric) weightedDerivative() float64 { var weight float64 d := tm.derivativeValue() + if d < 0 { weight = 1.0 } - return weight * d } +// Map the interval value down to an actual history index +func intervalToHistoryIndex(interval int) int { + return int(math.Floor(math.Log(float64(interval)) / math.Log(2))) +} + +// Retrieves the actual history data value that represents the requested time interval func (tm *TrustMetric) fadedMemoryValue(interval int) float64 { if interval == 0 { - // base case + // Base case return tm.history[0] } - - index := int(math.Floor(math.Log(float64(interval)) / math.Log(2))) - // map the interval value down to an actual history index - return tm.history[index] + return tm.history[intervalToHistoryIndex(interval)] } +// 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 } - // keep the last history element + // Keep the most recent history element faded := tm.history[:1] for i := 1; i < tm.historySize; i++ { + // The older the data is, the more we spread it out x := math.Pow(2, float64(i)) - + // Two history data values are merged into a single value ftv := ((tm.history[i] * (x - 1)) + tm.history[i-1]) / x - faded = append(faded, ftv) } tm.history = faded } -// calculates the integral (history) component of the trust value +// Calculates the integral (history) component of the trust value func (tm *TrustMetric) calcHistoryValue() float64 { var wk []float64 - // create the weights + // Create the weights. hlen := tm.numIntervals for i := 0; i < hlen; i++ { - x := math.Pow(.8, float64(i+1)) // optimistic wk + x := math.Pow(.8, float64(i+1)) // Optimistic weight wk = append(wk, x) } var wsum float64 - // calculate the sum of the weights + // Calculate the sum of the weights for _, v := range wk { wsum += v } var hv float64 - // calculate the history value + // Calculate the history value for i := 0; i < hlen; i++ { weight := wk[i] / wsum hv += tm.fadedMemoryValue(i) * weight @@ -255,10 +452,10 @@ func (tm *TrustMetric) calcHistoryValue() float64 { return hv } -// calculates the current score for good experiences +// Calculates the current score for good/bad experiences func (tm *TrustMetric) proportionalValue() float64 { value := 1.0 - // bad events are worth more + // Bad events are worth more in the calculation of our score total := tm.good + math.Pow(tm.bad, 2) if tm.bad > 0 || tm.good > 0 { @@ -267,37 +464,49 @@ func (tm *TrustMetric) proportionalValue() float64 { 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() + var t *time.Ticker + loop: for { select { case bg := <-tm.update: + // Check if this is the first experience with + // what we are tracking since being started or paused + if t == nil { + t = time.NewTicker(tm.intervalLen) + tm.good = 0 + tm.bad = 0 + } + if bg.IsBad { tm.bad += float64(bg.Add) } else { tm.good += float64(bg.Add) } case rtv := <-tm.trustValue: - // send the calculated trust value back rtv.Resp <- tm.calcTrustValue() case <-t.C: + // Add the current trust value to the history data newHist := tm.calcTrustValue() tm.history = append([]float64{newHist}, tm.history...) + // Update history and interval counters if tm.historySize < tm.historyMaxSize { tm.historySize++ } else { @@ -308,87 +517,26 @@ loop: tm.numIntervals++ } + // 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 - case <-tm.stop: - break loop + case stop := <-tm.stop: + if stop { + // Stop all further tracking for this metric + break loop + } + // Pause the metric for now by stopping the ticker + if t != nil { + t.Stop() + t = nil + } } } -} -func (tm *TrustMetric) Stop() { - tm.stop <- 1 -} - -// indicate that an undesirable event took place -func (tm *TrustMetric) IncBad() { - tm.update <- &updateBadGood{IsBad: true, Add: 1} -} - -// multiple undesirable events need to be acknowledged -func (tm *TrustMetric) AddBad(num int) { - tm.update <- &updateBadGood{IsBad: true, Add: num} -} - -// positive events need to be recorded as well -func (tm *TrustMetric) IncGood() { - tm.update <- &updateBadGood{IsBad: false, Add: 1} -} - -// multiple positive can be indicated in a single call -func (tm *TrustMetric) AddGood(num int) { - tm.update <- &updateBadGood{IsBad: false, Add: num} -} - -// get the dependable trust value; a score that takes a long history into account -func (tm *TrustMetric) TrustValue() float64 { - resp := make(chan float64, 1) - - tm.trustValue <- &reqTrustValue{Resp: resp} - return <-resp -} - -func NewMetric() *TrustMetric { - return NewMetricWithConfig(defaultConfig()) -} - -func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric { - tm := new(TrustMetric) - dc := defaultConfig() - - if tmc.ProportionalWeight != 0 { - tm.proportionalWeight = tmc.ProportionalWeight - } else { - tm.proportionalWeight = dc.ProportionalWeight - } - - if tmc.IntegralWeight != 0 { - tm.integralWeight = tmc.IntegralWeight - } else { - tm.integralWeight = dc.IntegralWeight - } - - if tmc.HistoryMaxSize != 0 { - tm.historyMaxSize = tmc.HistoryMaxSize - } else { - tm.historyMaxSize = dc.HistoryMaxSize + if t != nil { + t.Stop() } - - if tmc.IntervalLen != time.Duration(0) { - tm.intervalLen = tmc.IntervalLen - } else { - tm.intervalLen = dc.IntervalLen - } - - // this gives our metric a tracking window of days - tm.maxIntervals = int(math.Pow(2, float64(tm.historyMaxSize))) - tm.historyValue = 1.0 - tm.update = make(chan *updateBadGood, 10) - tm.trustValue = make(chan *reqTrustValue, 10) - tm.stop = make(chan int, 1) - - go tm.processRequests() - return tm } From 687834c99e0bfcbef4a5f183ebf6900a774355a8 Mon Sep 17 00:00:00 2001 From: caffix Date: Mon, 30 Oct 2017 18:45:54 -0400 Subject: [PATCH 3/5] added initial trust metric test routines --- config/config.go | 9 - docs/architecture/adr-006-trust-metric.md | 177 +++++++++++----- node/node.go | 29 ++- p2p/trust/trustmetric.go | 205 ++++++++++--------- p2p/trust/trustmetric_test.go | 239 ++++++++++++++++++++++ 5 files changed, 487 insertions(+), 172 deletions(-) create mode 100644 p2p/trust/trustmetric_test.go diff --git a/config/config.go b/config/config.go index 46fb55ec6..25d6c44a5 100644 --- a/config/config.go +++ b/config/config.go @@ -214,9 +214,6 @@ type P2PConfig struct { // Set true for strict address routability rules AddrBookStrict bool `mapstructure:"addr_book_strict"` - // Path to the trust history file - TrustHistory string `mapstructure:"trust_history_file"` - // Set true to enable the peer-exchange reactor PexReactor bool `mapstructure:"pex"` @@ -242,7 +239,6 @@ func DefaultP2PConfig() *P2PConfig { ListenAddress: "tcp://0.0.0.0:46656", AddrBook: "addrbook.json", AddrBookStrict: true, - TrustHistory: "trusthistory.json", MaxNumPeers: 50, FlushThrottleTimeout: 100, MaxMsgPacketPayloadSize: 1024, // 1 kB @@ -264,11 +260,6 @@ func (p *P2PConfig) AddrBookFile() string { return rootify(p.AddrBook, p.RootDir) } -// TrustHistoryFile returns the full path to the trust metric store history -func (p *P2PConfig) TrustHistoryFile() string { - return rootify(p.TrustHistory, p.RootDir) -} - //----------------------------------------------------------------------------- // MempoolConfig diff --git a/docs/architecture/adr-006-trust-metric.md b/docs/architecture/adr-006-trust-metric.md index 6fc5f9ea6..961830cf9 100644 --- a/docs/architecture/adr-006-trust-metric.md +++ b/docs/architecture/adr-006-trust-metric.md @@ -1,10 +1,10 @@ -# Trust Metric Design +# ADR 006: Trust Metric Design -## Overview +## Context The proposed trust metric will allow Tendermint to maintain local trust rankings for peers it has directly interacted with, which can then be used to implement soft security controls. The calculations were obtained from the [TrustGuard](https://dl.acm.org/citation.cfm?id=1060808) project. -## Background +### Background The Tendermint Core project developers would like to improve Tendermint security and reliability by keeping track of the level of trustworthiness peers have demonstrated within the peer-to-peer network. This way, undesirable outcomes from peers will not immediately result in them being dropped from the network (potentially causing drastic changes to take place). Instead, peers behavior can be monitored with appropriate metrics and be removed from the network once Tendermint Core is certain the peer is a threat. For example, when the PEXReactor makes a request for peers network addresses from a already known peer, and the returned network addresses are unreachable, this untrustworthy behavior should be tracked. Returning a few bad network addresses probably shouldn’t cause a peer to be dropped, while excessive amounts of this behavior does qualify the peer being dropped. @@ -12,13 +12,15 @@ Trust metrics can be circumvented by malicious nodes through the use of strategi Instead, having shorter intervals, but keeping a history of interval values, will give our metric the flexibility needed in order to keep the network stable, while also making it resilient against a strategic malicious node in the Tendermint peer-to-peer network. Also, the metric can access trust data over a rather long period of time while not greatly increasing its history size by aggregating older history values over a larger number of intervals, and at the same time, maintain great precision for the recent intervals. This approach is referred to as fading memories, and closely resembles the way human beings remember their experiences. The trade-off to using history data is that the interval values should be preserved in-between executions of the node. -## Scope +### References -The proposed trust metric will be implemented as a Go programming language object that will allow a developer to inform the object of all good and bad events relevant to the trust object instantiation, and at any time, the metric can be queried for the current trust ranking. Methods will be provided for storing trust metric history data that is required across instantiations. +S. Mudhakar, L. Xiong, and L. Liu, “TrustGuard: Countering Vulnerabilities in Reputation Management for Decentralized Overlay Networks,” in *Proceedings of the 14th international conference on World Wide Web, pp. 422-431*, May 2005. -## Detailed Design +## Decision -This section will cover the process being considered for calculating the trust ranking and the interface for the trust metric. +The proposed trust metric will allow a developer to inform the trust metric store of all good and bad events relevant to a peer's behavior, and at any time, the metric can be queried for a peer's current trust ranking. + +The three subsections below will cover the process being considered for calculating the trust ranking, the concept of the trust metric store, and the interface for the trust metric. ### Proposed Process @@ -33,7 +35,7 @@ The equation being proposed resembles a Proportional-Integral-Derivative (PID) c where *R*[*i*] denotes the raw trust value at time interval *i* (where *i* == 0 being current time) and *a* is the weight applied to the contribution of the current reports. The next component of our equation uses a weighted sum over the last *maxH* intervals to calculate the history value for time *i*: -`H[i] = ` ![formula1](https://github.com/tendermint/tendermint/blob/develop/docs/architecture/img/formula1.png "Weighted Sum Formula") +`H[i] = ` ![formula1](img/formula1.png "Weighted Sum Formula") The weights can be chosen either optimistically or pessimistically. With the history value available, we can now finish calculating the integral value: @@ -68,75 +70,71 @@ Where *j* is one of *(0, 1, 2, … , m – 1)* indices used to access history in R[0] = raw data for current time interval ``` -`R[j] = ` ![formula2](https://github.com/tendermint/tendermint/blob/develop/docs/architecture/img/formula2.png "Fading Memories Formula") - +`R[j] = ` ![formula2](img/formula2.png "Fading Memories Formula") -### Interface Detailed Design +### Trust Metric Store -This section will cover the Go programming language API designed for the previously proposed process. Below is the interface for a TrustMetric: - -```go +Similar to the P2P subsystem AddrBook, the trust metric store will maintain information relevant to Tendermint peers. Additionally, the trust metric store will ensure that trust metrics will only be active for peers that a node is currently and directly engaged with. -package trust +Reactors will provide a peer key to the trust metric store in order to retrieve the associated trust metric. The trust metric can then record new positive and negative events experienced by the reactor, as well as provided the current trust score calculated by the metric. +When the node is shutting down, the trust metric store will save history data for trust metrics associated with all known peers. This saved information allows experiences with a peer to be preserved across node executions, which can span a tracking windows of days or weeks. The trust history data is loaded automatically during OnStart. -// TrustMetricStore - Manages all trust metrics for peers -type TrustMetricStore struct { - cmn.BaseService - - // Private elements -} - -// OnStart implements Service -func (tms *TrustMetricStore) OnStart() error - -/ OnStop implements Service -func (tms *TrustMetricStore) OnStop() +### Interface Detailed Design -// NewTrustMetricStore returns a store that optionally saves data to -// the file path and uses the optional config when creating new trust metrics -func NewTrustMetricStore(filePath string, tmc *TrustMetricConfig) *TrustMetricStore +Each trust metric allows for the recording of positive/negative events, querying the current trust value/score, and the stopping/pausing of tracking over time intervals. This can be seen below: -// GetPeerTrustMetric returns a trust metric by peer key -func (tms *TrustMetricStore) GetPeerTrustMetric(key string) *TrustMetric - -// PeerDisconnected pauses the trust metric associated with the peer identified by the key -func (tms *TrustMetricStore) PeerDisconnected(key string) +```go -//---------------------------------------------------------------------------------------- // TrustMetric - keeps track of peer reliability type TrustMetric struct { // Private elements. } -// Pause tells the metric to pause recording data over time intervals -func (tm *TrustMetric) Pause() +// 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() {} // Stop tells the metric to stop recording data over time intervals -func (tm *TrustMetric) Stop() +func (tm *TrustMetric) Stop() {} // BadEvent indicates that an undesirable event took place -func (tm *TrustMetric) BadEvent() +func (tm *TrustMetric) BadEvent() {} // AddBadEvents acknowledges multiple undesirable events -func (tm *TrustMetric) AddBadEvents(num int) +func (tm *TrustMetric) AddBadEvents(num int) {} // GoodEvent indicates that a desirable event took place -func (tm *TrustMetric) GoodEvent() +func (tm *TrustMetric) GoodEvent() {} // AddGoodEvents acknowledges multiple desirable events -func (tm *TrustMetric) AddGoodEvents(num int) +func (tm *TrustMetric) AddGoodEvents(num int) {} // TrustValue gets the dependable trust value; always between 0 and 1 -func (tm *TrustMetric) TrustValue() float64 +func (tm *TrustMetric) TrustValue() float64 {} // TrustScore gets a score based on the trust value always between 0 and 100 -func (tm *TrustMetric) TrustScore() int +func (tm *TrustMetric) TrustScore() int {} // NewMetric returns a trust metric with the default configuration -func NewMetric() *TrustMetric +func NewMetric() *TrustMetric {} + +//------------------------------------------------------------------------------------------------ +// For example + +tm := NewMetric() + +tm.BadEvent() +score := tm.TrustScore() + +tm.Stop() + +``` +Some of the trust metric parameters can be configured. The weight values should probably be left alone in more cases, yet the time durations for the tracking window and individual time interval should be considered. + +```go // TrustMetricConfig - Configures the weight functions and time intervals for the metric type TrustMetricConfig struct { @@ -153,17 +151,94 @@ type TrustMetricConfig struct { // Each interval should be short for adapability. // Less than 30 seconds is too sensitive, // and greater than 5 minutes will make the metric numb - IntervalLen time.Duration + IntervalLength time.Duration } // DefaultConfig returns a config with values that have been tested and produce desirable results -func DefaultConfig() *TrustMetricConfig +func DefaultConfig() TrustMetricConfig {} // NewMetricWithConfig returns a trust metric with a custom configuration -func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric +func NewMetricWithConfig(tmc TrustMetricConfig) *TrustMetric {} + +//------------------------------------------------------------------------------------------------ +// For example + +config := TrustMetricConfig{ + TrackingWindow: time.Minute * 60 * 24, // one day + IntervalLength: time.Minute * 2, +} + +tm := NewMetricWithConfig(config) + +tm.AddBadEvents(10) +tm.Pause() +tm.GoodEvent() // becomes active again + +``` + +A trust metric store should be created with a DB that has persistent storage so it can save history data across node executions. All trust metrics instantiated by the store will be created with the provided TrustMetricConfig configuration. + +When you attempt to fetch the trust metric for a peer, and an entry does not exist in the trust metric store, a new metric is automatically created and the entry made within the store. + +In additional to the fetching method, GetPeerTrustMetric, the trust metric store provides a method to call when a peer has disconnected from the node. This is so the metric can be paused (history data will not be saved) for periods of time when the node is not having direct experiences with the peer. + +```go + +// TrustMetricStore - Manages all trust metrics for peers +type TrustMetricStore struct { + cmn.BaseService + + // Private elements +} + +// OnStart implements Service +func (tms *TrustMetricStore) OnStart() error {} + +// OnStop implements Service +func (tms *TrustMetricStore) OnStop() {} + +// 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 {} + +// Size returns the number of entries in the trust metric store +func (tms *TrustMetricStore) Size() int {} + +// GetPeerTrustMetric returns a trust metric by peer key +func (tms *TrustMetricStore) GetPeerTrustMetric(key string) *TrustMetric {} + +// PeerDisconnected pauses the trust metric associated with the peer identified by the key +func (tms *TrustMetricStore) PeerDisconnected(key string) {} + +//------------------------------------------------------------------------------------------------ +// For example + +db := dbm.NewDB("trusthistory", "goleveldb", dirPathStr) +tms := NewTrustMetricStore(db, DefaultConfig()) + +tm := tms.GetPeerTrustMetric(key) +tm.BadEvent() + +tms.PeerDisconnected(key) ``` -## References +## Status + +Proposed. + +## Consequences + +### Positive + +- The trust metric will allow Tendermint to make non-binary security and reliability decisions +- Will help Tendermint implement deterrents that provide soft security controls, yet avoids disruption on the network +- Will provide useful profiling information when analyzing performance over time related to peer interaction + +### Negative + +- Requires saving the trust metric history data across node executions + +### Neutral -S. Mudhakar, L. Xiong, and L. Liu, “TrustGuard: Countering Vulnerabilities in Reputation Management for Decentralized Overlay Networks,” in *Proceedings of the 14th international conference on World Wide Web, pp. 422-431*, May 2005. \ No newline at end of file +- Keep in mind that, good events need to be recorded just as bad events do using this implementation diff --git a/node/node.go b/node/node.go index 29be71caf..97e0693e0 100644 --- a/node/node.go +++ b/node/node.go @@ -96,10 +96,10 @@ type Node struct { privValidator types.PrivValidator // local node's validator key // network - privKey crypto.PrivKeyEd25519 // local node's p2p key - sw *p2p.Switch // p2p connections - addrBook *p2p.AddrBook // known peers - tmStore *trust.TrustMetricStore // trust metrics for all peers + privKey crypto.PrivKeyEd25519 // local node's p2p key + sw *p2p.Switch // p2p connections + addrBook *p2p.AddrBook // known peers + trustMetricStore *trust.TrustMetricStore // trust metrics for all peers // services eventBus *types.EventBus // pub/sub for services @@ -241,12 +241,19 @@ func NewNode(config *cfg.Config, // Optionally, start the pex reactor var addrBook *p2p.AddrBook - var tmStore *trust.TrustMetricStore + var trustMetricStore *trust.TrustMetricStore if config.P2P.PexReactor { addrBook = p2p.NewAddrBook(config.P2P.AddrBookFile(), config.P2P.AddrBookStrict) addrBook.SetLogger(p2pLogger.With("book", config.P2P.AddrBookFile())) - tmStore = trust.NewTrustMetricStore(config.P2P.TrustHistoryFile(), nil) - tmStore.SetLogger(p2pLogger.With("trust", config.P2P.TrustHistoryFile())) + + // Get the trust metric history data + trustHistoryDB, err := dbProvider(&DBContext{"trusthistory", config}) + if err != nil { + return nil, err + } + trustMetricStore = trust.NewTrustMetricStore(trustHistoryDB, trust.DefaultConfig()) + trustMetricStore.SetLogger(p2pLogger) + pexReactor := p2p.NewPEXReactor(addrBook) pexReactor.SetLogger(p2pLogger) sw.AddReactor("PEX", pexReactor) @@ -299,10 +306,10 @@ func NewNode(config *cfg.Config, genesisDoc: genDoc, privValidator: privValidator, - privKey: privKey, - sw: sw, - addrBook: addrBook, - tmStore: tmStore, + privKey: privKey, + sw: sw, + addrBook: addrBook, + trustMetricStore: trustMetricStore, blockStore: blockStore, bcReactor: bcReactor, diff --git a/p2p/trust/trustmetric.go b/p2p/trust/trustmetric.go index e4f202bb2..6733996b0 100644 --- a/p2p/trust/trustmetric.go +++ b/p2p/trust/trustmetric.go @@ -5,12 +5,12 @@ package trust import ( "encoding/json" - "io/ioutil" "math" "sync" "time" cmn "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" ) // TrustMetricStore - Manages all trust metrics for peers @@ -23,19 +23,19 @@ type TrustMetricStore struct { // Mutex that protects the map and history data file mtx sync.Mutex - // The file path where peer trust metric history data will be stored - filePath string + // 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 + config TrustMetricConfig } -// NewTrustMetricStore returns a store that optionally saves data to -// the file path and uses the optional config when creating new trust metrics -func NewTrustMetricStore(filePath string, tmc *TrustMetricConfig) *TrustMetricStore { +// 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), - filePath: filePath, + db: db, config: tmc, } @@ -49,7 +49,8 @@ func (tms *TrustMetricStore) OnStart() error { tms.mtx.Lock() defer tms.mtx.Unlock() - tms.loadFromFile() + + tms.loadFromDB() return nil } @@ -63,10 +64,18 @@ func (tms *TrustMetricStore) OnStop() { tm.Stop() } - tms.saveToFile() + tms.saveToDB() tms.BaseService.OnStop() } +// 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() @@ -95,41 +104,43 @@ func (tms *TrustMetricStore) PeerDisconnected(key string) { } } +/* 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 of these methods assume the mutex has been acquired, since they write to the map */ + +var trustMetricKey = []byte("trustMetricStore") type peerHistoryJSON struct { NumIntervals int `json:"intervals"` History []float64 `json:"history"` } -// Loads the history data for the Peer identified by key from the store file. +// Loads the history data for the Peer identified by key from the store DB. // cmn.Panics if file is corrupt -func (tms *TrustMetricStore) loadFromFile() bool { - // Check that a file has been configured for use - if tms.filePath == "" { - // The trust metric store can operate without the file - return false - } - +func (tms *TrustMetricStore) loadFromDB() bool { // Obtain the history data we have so far - content, err := ioutil.ReadFile(tms.filePath) - if err != nil { - cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", tms.filePath, err)) + bytes := tms.db.Get(trustMetricKey) + if bytes == nil { + return false } peers := make(map[string]peerHistoryJSON, 0) - err = json.Unmarshal(content, &peers) + err := json.Unmarshal(bytes, &peers) if err != nil { - cmn.PanicCrisis(cmn.Fmt("Error decoding file %s: %v", tms.filePath, err)) + cmn.PanicCrisis(cmn.Fmt("Could not unmarchal Trust Metric Store DB data: %v", err)) } // If history data exists in the file, // load it into trust metrics and recalc for key, p := range peers { tm := NewMetricWithConfig(tms.config) - if tm == nil { - continue - } + // Restore the number of time intervals we have previously tracked if p.NumIntervals > tm.maxIntervals { p.NumIntervals = tm.maxIntervals @@ -149,15 +160,9 @@ func (tms *TrustMetricStore) loadFromFile() bool { return true } -// Saves the history data for all peers to the store file -func (tms *TrustMetricStore) saveToFile() { - // Check that a file has been configured for use - if tms.filePath == "" { - // The trust metric store can operate without the file - return - } - - tms.Logger.Info("Saving TrustHistory to file", "size", len(tms.peerMetrics)) +// Saves the history data for all peers to the store DB +func (tms *TrustMetricStore) saveToDB() { + tms.Logger.Info("Saving TrustHistory to DB", "size", tms.size()) peers := make(map[string]peerHistoryJSON, 0) @@ -169,20 +174,23 @@ func (tms *TrustMetricStore) saveToFile() { } } - // Write all the data back to the file - b, err := json.Marshal(peers) + // 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 } - - err = ioutil.WriteFile(tms.filePath, b, 0644) - if err != nil { - tms.Logger.Error("Failed to save TrustHistory to file", "err", err) - } + tms.db.SetSync(trustMetricKey, bytes) } //--------------------------------------------------------------------------------------- + +// The number of event updates that can be sent on a single metric before blocking +const defaultUpdateChanCapacity = 10 + +// The number of trust value requests that can be made simultaneously before blocking +const defaultRequestChanCapacity = 10 + // TrustMetric - keeps track of peer reliability // See tendermint/docs/architecture/adr-006-trust-metric.md for details type TrustMetric struct { @@ -216,6 +224,9 @@ type TrustMetric struct { // 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 + // Sending true on this channel stops tracking, while false pauses tracking stop chan bool @@ -238,7 +249,8 @@ type reqTrustValue struct { Resp chan float64 } -// Pause tells the metric to pause recording data over time intervals +// 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.stop <- false } @@ -299,40 +311,33 @@ type TrustMetricConfig struct { // Each interval should be short for adapability. // Less than 30 seconds is too sensitive, // and greater than 5 minutes will make the metric numb - IntervalLen time.Duration + IntervalLength time.Duration } // DefaultConfig returns a config with values that have been tested and produce desirable results -func DefaultConfig() *TrustMetricConfig { - return &TrustMetricConfig{ +func DefaultConfig() TrustMetricConfig { + return TrustMetricConfig{ ProportionalWeight: 0.4, IntegralWeight: 0.6, TrackingWindow: (time.Minute * 60 * 24) * 14, // 14 days. - IntervalLen: 1 * time.Minute, + IntervalLength: 1 * time.Minute, } } // NewMetric returns a trust metric with the default configuration func NewMetric() *TrustMetric { - return NewMetricWithConfig(nil) + return NewMetricWithConfig(DefaultConfig()) } // NewMetricWithConfig returns a trust metric with a custom configuration -func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric { - var config *TrustMetricConfig - - if tmc == nil { - config = DefaultConfig() - } else { - config = customConfig(tmc) - } - +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.IntervalLen + 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 @@ -340,8 +345,8 @@ func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric { // This metric has a perfect history so far tm.historyValue = 1.0 // Setup the channels - tm.update = make(chan *updateBadGood, 10) - tm.trustValue = make(chan *reqTrustValue, 10) + tm.update = make(chan *updateBadGood, defaultUpdateChanCapacity) + tm.trustValue = make(chan *reqTrustValue, defaultRequestChanCapacity) tm.stop = make(chan bool, 2) go tm.processRequests() @@ -351,25 +356,27 @@ func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric { /* Private methods */ // Ensures that all configuration elements have valid values -func customConfig(tmc *TrustMetricConfig) *TrustMetricConfig { +func customConfig(tmc TrustMetricConfig) TrustMetricConfig { config := DefaultConfig() // Check the config for set values, and setup appropriately - if tmc.ProportionalWeight != 0 { + if tmc.ProportionalWeight > 0 { config.ProportionalWeight = tmc.ProportionalWeight } - if tmc.IntegralWeight != 0 { + if tmc.IntegralWeight > 0 { config.IntegralWeight = tmc.IntegralWeight } - if tmc.TrackingWindow != time.Duration(0) { - config.TrackingWindow = tmc.TrackingWindow + if tmc.IntervalLength > time.Duration(0) { + config.IntervalLength = tmc.IntervalLength } - if tmc.IntervalLen != time.Duration(0) { - config.IntervalLen = tmc.IntervalLen + if tmc.TrackingWindow > time.Duration(0) && + tmc.TrackingWindow >= config.IntervalLength { + config.TrackingWindow = tmc.TrackingWindow } + return config } @@ -480,18 +487,19 @@ func (tm *TrustMetric) calcTrustValue() float64 { // This method is for a goroutine that handles all requests on the metric func (tm *TrustMetric) processRequests() { - var t *time.Ticker - + t := time.NewTicker(tm.intervalLen) + defer t.Stop() loop: for { select { case bg := <-tm.update: // Check if this is the first experience with - // what we are tracking since being started or paused - if t == nil { - t = time.NewTicker(tm.intervalLen) + // 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 } if bg.IsBad { @@ -502,41 +510,36 @@ loop: case rtv := <-tm.trustValue: rtv.Resp <- tm.calcTrustValue() case <-t.C: - // Add the current trust value to the history data - newHist := tm.calcTrustValue() - tm.history = append([]float64{newHist}, tm.history...) - - // Update history and interval counters - if tm.historySize < tm.historyMaxSize { - tm.historySize++ - } else { - tm.history = tm.history[:tm.historyMaxSize] - } - - if tm.numIntervals < tm.maxIntervals { - tm.numIntervals++ + if !tm.paused { + // Add the current trust value to the history data + newHist := tm.calcTrustValue() + tm.history = append([]float64{newHist}, tm.history...) + + // Update history and interval counters + if tm.historySize < tm.historyMaxSize { + tm.historySize++ + } else { + tm.history = tm.history[:tm.historyMaxSize] + } + + if tm.numIntervals < tm.maxIntervals { + tm.numIntervals++ + } + + // 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 } - - // 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 case stop := <-tm.stop: if stop { // Stop all further tracking for this metric break loop } - // Pause the metric for now by stopping the ticker - if t != nil { - t.Stop() - t = nil - } + // Pause the metric for now + tm.paused = true } } - - if t != nil { - t.Stop() - } } diff --git a/p2p/trust/trustmetric_test.go b/p2p/trust/trustmetric_test.go new file mode 100644 index 000000000..9c61bec96 --- /dev/null +++ b/p2p/trust/trustmetric_test.go @@ -0,0 +1,239 @@ +// 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 ( + "fmt" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/log" +) + +func getTempDir(prefix string) string { + dir, err := ioutil.TempDir("", prefix) + if err != nil { + panic(err) + } + return dir +} + +func TestTrustMetricStoreSaveLoad(t *testing.T) { + dir := getTempDir("trustMetricStoreTest") + defer os.Remove(dir) + + historyDB := dbm.NewDB("trusthistory", "goleveldb", dir) + + config := TrustMetricConfig{ + TrackingWindow: 5 * time.Minute, + IntervalLength: 50 * time.Millisecond, + } + + // 0 peers saved + store := NewTrustMetricStore(historyDB, config) + store.SetLogger(log.TestingLogger()) + store.saveToDB() + // Load the data from the file + store = NewTrustMetricStore(historyDB, config) + store.SetLogger(log.TestingLogger()) + store.loadFromDB() + // Make sure we still have 0 entries + assert.Zero(t, store.Size()) + + // 100 peers + for i := 0; i < 100; i++ { + key := fmt.Sprintf("peer_%d", i) + tm := store.GetPeerTrustMetric(key) + + tm.AddBadEvents(10) + tm.GoodEvent() + } + + // Check that we have 100 entries and save + assert.Equal(t, 100, store.Size()) + // Give the metrics time to process the history data + time.Sleep(1 * time.Second) + + // Stop all the trust metrics and save + for _, tm := range store.peerMetrics { + tm.Stop() + } + store.saveToDB() + + // Load the data from the DB + store = NewTrustMetricStore(historyDB, config) + store.SetLogger(log.TestingLogger()) + store.loadFromDB() + + // Check that we still have 100 peers with imperfect trust values + assert.Equal(t, 100, store.Size()) + for _, tm := range store.peerMetrics { + assert.NotEqual(t, 1.0, tm.TrustValue()) + } + + // Stop all the trust metrics + for _, tm := range store.peerMetrics { + tm.Stop() + } +} + +func TestTrustMetricStoreConfig(t *testing.T) { + historyDB := dbm.NewDB("", "memdb", "") + + config := TrustMetricConfig{ + ProportionalWeight: 0.5, + IntegralWeight: 0.5, + } + + // Create a store with custom config + store := NewTrustMetricStore(historyDB, config) + store.SetLogger(log.TestingLogger()) + + // Have the store make us a metric with the config + tm := store.GetPeerTrustMetric("TestKey") + + // Check that the options made it to the metric + assert.Equal(t, 0.5, tm.proportionalWeight) + assert.Equal(t, 0.5, tm.integralWeight) + tm.Stop() +} + +func TestTrustMetricStoreLookup(t *testing.T) { + historyDB := dbm.NewDB("", "memdb", "") + + store := NewTrustMetricStore(historyDB, DefaultConfig()) + store.SetLogger(log.TestingLogger()) + + // Create 100 peers in the trust metric store + for i := 0; i < 100; i++ { + key := fmt.Sprintf("peer_%d", i) + store.GetPeerTrustMetric(key) + + // Check that the trust metric was successfully entered + ktm := store.peerMetrics[key] + assert.NotNil(t, ktm, "Expected to find TrustMetric %s but wasn't there.", key) + } + + // Stop all the trust metrics + for _, tm := range store.peerMetrics { + tm.Stop() + } +} + +func TestTrustMetricStorePeerScore(t *testing.T) { + historyDB := dbm.NewDB("", "memdb", "") + + store := NewTrustMetricStore(historyDB, DefaultConfig()) + store.SetLogger(log.TestingLogger()) + + key := "TestKey" + tm := store.GetPeerTrustMetric(key) + + // This peer is innocent so far + first := tm.TrustScore() + assert.Equal(t, 100, first) + + // Add some undesirable events and disconnect + tm.BadEvent() + first = tm.TrustScore() + assert.NotEqual(t, 100, first) + tm.AddBadEvents(10) + second := tm.TrustScore() + + if second > first { + t.Errorf("A greater number of bad events should lower the trust score") + } + store.PeerDisconnected(key) + + // We will remember our experiences with this peer + tm = store.GetPeerTrustMetric(key) + assert.NotEqual(t, 100, tm.TrustScore()) + tm.Stop() +} + +func TestTrustMetricScores(t *testing.T) { + tm := NewMetric() + + // Perfect score + tm.GoodEvent() + score := tm.TrustScore() + assert.Equal(t, 100, score) + + // Less than perfect score + tm.AddBadEvents(10) + score = tm.TrustScore() + assert.NotEqual(t, 100, score) + tm.Stop() +} + +func TestTrustMetricConfig(t *testing.T) { + // 7 days + window := time.Minute * 60 * 24 * 7 + config := TrustMetricConfig{ + TrackingWindow: window, + IntervalLength: 2 * time.Minute, + } + + tm := NewMetricWithConfig(config) + + // The max time intervals should be the TrackingWindow / IntervalLen + assert.Equal(t, int(config.TrackingWindow/config.IntervalLength), tm.maxIntervals) + + dc := DefaultConfig() + // These weights should still be the default values + assert.Equal(t, dc.ProportionalWeight, tm.proportionalWeight) + assert.Equal(t, dc.IntegralWeight, tm.integralWeight) + tm.Stop() + + config.ProportionalWeight = 0.3 + config.IntegralWeight = 0.7 + tm = NewMetricWithConfig(config) + + // These weights should be equal to our custom values + assert.Equal(t, config.ProportionalWeight, tm.proportionalWeight) + assert.Equal(t, config.IntegralWeight, tm.integralWeight) + tm.Stop() +} + +func TestTrustMetricStopPause(t *testing.T) { + // Cause time intervals to pass quickly + config := TrustMetricConfig{ + TrackingWindow: 5 * time.Minute, + IntervalLength: 10 * time.Millisecond, + } + + tm := NewMetricWithConfig(config) + + // Allow some time intervals to pass and pause + time.Sleep(50 * time.Millisecond) + tm.Pause() + // Give the pause some time to take place + time.Sleep(10 * time.Millisecond) + + first := tm.numIntervals + // Allow more time to pass and check the intervals are unchanged + time.Sleep(50 * time.Millisecond) + assert.Equal(t, first, tm.numIntervals) + + // Get the trust metric activated again + tm.AddGoodEvents(5) + // Allow some time intervals to pass and stop + time.Sleep(50 * time.Millisecond) + tm.Stop() + // Give the stop some time to take place + time.Sleep(10 * time.Millisecond) + + second := tm.numIntervals + // Allow more time to pass and check the intervals are unchanged + time.Sleep(50 * time.Millisecond) + assert.Equal(t, second, tm.numIntervals) + + if first >= second { + t.Fatalf("numIntervals should always increase or stay the same over time") + } +} From 8b7649b90c9f1dc28c74ce30844e19a72704d356 Mon Sep 17 00:00:00 2001 From: caffix Date: Wed, 8 Nov 2017 16:03:06 -0500 Subject: [PATCH 4/5] enhancements made in response to PR full review comments --- p2p/trust/trustmetric.go | 89 +++++++++++++++++++++++------------ p2p/trust/trustmetric_test.go | 9 +--- 2 files changed, 61 insertions(+), 37 deletions(-) diff --git a/p2p/trust/trustmetric.go b/p2p/trust/trustmetric.go index 6733996b0..39c24c247 100644 --- a/p2p/trust/trustmetric.go +++ b/p2p/trust/trustmetric.go @@ -13,6 +13,8 @@ import ( dbm "github.com/tendermint/tmlibs/db" ) +const defaultStorePeriodicSaveInterval = 1 * time.Minute + // TrustMetricStore - Manages all trust metrics for peers type TrustMetricStore struct { cmn.BaseService @@ -28,6 +30,9 @@ type TrustMetricStore struct { // This configuration will be used when creating new TrustMetrics config TrustMetricConfig + + // This channel is used to stop the store go-routine + stop chan int } // NewTrustMetricStore returns a store that saves data to the DB @@ -37,6 +42,7 @@ func NewTrustMetricStore(db dbm.DB, tmc TrustMetricConfig) *TrustMetricStore { peerMetrics: make(map[string]*TrustMetric), db: db, config: tmc, + stop: make(chan int, 2), } tms.BaseService = *cmn.NewBaseService(nil, "TrustMetricStore", tms) @@ -51,19 +57,24 @@ func (tms *TrustMetricStore) OnStart() error { defer tms.mtx.Unlock() tms.loadFromDB() + go tms.periodicSave() return nil } // OnStop implements Service func (tms *TrustMetricStore) OnStop() { + // Stop the store periodic save go-routine + tms.stop <- 1 + tms.mtx.Lock() defer tms.mtx.Unlock() - // Stop all trust metric goroutines + // Stop all trust metric go-routines for _, tm := range tms.peerMetrics { tm.Stop() } + // Make the final trust history data save tms.saveToDB() tms.BaseService.OnStop() } @@ -85,10 +96,8 @@ func (tms *TrustMetricStore) GetPeerTrustMetric(key string) *TrustMetric { if !ok { // If the metric is not available, we will create it tm = NewMetricWithConfig(tms.config) - if tm != nil { - // The metric needs to be in the map - tms.peerMetrics[key] = tm - } + // The metric needs to be in the map + tms.peerMetrics[key] = tm } return tm } @@ -133,7 +142,7 @@ func (tms *TrustMetricStore) loadFromDB() bool { peers := make(map[string]peerHistoryJSON, 0) err := json.Unmarshal(bytes, &peers) if err != nil { - cmn.PanicCrisis(cmn.Fmt("Could not unmarchal Trust Metric Store DB data: %v", err)) + cmn.PanicCrisis(cmn.Fmt("Could not unmarshal Trust Metric Store DB data: %v", err)) } // If history data exists in the file, @@ -183,6 +192,23 @@ func (tms *TrustMetricStore) saveToDB() { tms.db.SetSync(trustMetricKey, bytes) } +// Periodically saves the trust history data to the DB +func (tms *TrustMetricStore) periodicSave() { + t := time.NewTicker(defaultStorePeriodicSaveInterval) + defer t.Stop() +loop: + for { + select { + case <-t.C: + tms.mtx.Lock() + tms.saveToDB() + tms.mtx.Unlock() + case <-tms.stop: + break loop + } + } +} + //--------------------------------------------------------------------------------------- // The number of event updates that can be sent on a single metric before blocking @@ -341,7 +367,7 @@ func NewMetricWithConfig(tmc TrustMetricConfig) *TrustMetric { // 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 = intervalToHistoryIndex(tm.maxIntervals) + 1 + tm.historyMaxSize = intervalToHistoryOffset(tm.maxIntervals) + 1 // This metric has a perfect history so far tm.historyValue = 1.0 // Setup the channels @@ -397,20 +423,6 @@ func (tm *TrustMetric) weightedDerivative() float64 { return weight * d } -// Map the interval value down to an actual history index -func intervalToHistoryIndex(interval int) int { - return int(math.Floor(math.Log(float64(interval)) / math.Log(2))) -} - -// Retrieves the actual history data value that represents the requested time interval -func (tm *TrustMetric) fadedMemoryValue(interval int) float64 { - if interval == 0 { - // Base case - return tm.history[0] - } - return tm.history[intervalToHistoryIndex(interval)] -} - // 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 @@ -419,18 +431,32 @@ func (tm *TrustMetric) updateFadedMemory() { return } + first := tm.historySize - 1 // Keep the most recent history element - faded := tm.history[:1] - - for i := 1; i < tm.historySize; i++ { + for count, i := 1, first-1; count < tm.historySize; count, i = count+1, i-1 { // The older the data is, the more we spread it out - x := math.Pow(2, float64(i)) + x := math.Pow(2, float64(count)) // Two history data values are merged into a single value - ftv := ((tm.history[i] * (x - 1)) + tm.history[i-1]) / x - faded = append(faded, ftv) + 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 { + return int(math.Floor(math.Log(float64(interval)) / math.Log(2))) +} + +// 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] } - tm.history = faded + offset := intervalToHistoryOffset(interval) + return tm.history[first-offset] } // Calculates the integral (history) component of the trust value @@ -513,13 +539,16 @@ loop: if !tm.paused { // Add the current trust value to the history data newHist := tm.calcTrustValue() - tm.history = append([]float64{newHist}, tm.history...) + tm.history = append(tm.history, newHist) // Update history and interval counters if tm.historySize < tm.historyMaxSize { tm.historySize++ } else { - tm.history = tm.history[:tm.historyMaxSize] + last := len(tm.history) - tm.historyMaxSize + + // Keep the history no larger than historyMaxSize + tm.history = tm.history[last:] } if tm.numIntervals < tm.maxIntervals { diff --git a/p2p/trust/trustmetric_test.go b/p2p/trust/trustmetric_test.go index 9c61bec96..626ca3bd5 100644 --- a/p2p/trust/trustmetric_test.go +++ b/p2p/trust/trustmetric_test.go @@ -15,16 +15,11 @@ import ( "github.com/tendermint/tmlibs/log" ) -func getTempDir(prefix string) string { - dir, err := ioutil.TempDir("", prefix) +func TestTrustMetricStoreSaveLoad(t *testing.T) { + dir, err := ioutil.TempDir("", "trust_test") if err != nil { panic(err) } - return dir -} - -func TestTrustMetricStoreSaveLoad(t *testing.T) { - dir := getTempDir("trustMetricStoreTest") defer os.Remove(dir) historyDB := dbm.NewDB("trusthistory", "goleveldb", dir) From a724ffab25c266dc9e9c6af8f889c9d49b3a28c7 Mon Sep 17 00:00:00 2001 From: caffix Date: Wed, 15 Nov 2017 17:59:48 -0500 Subject: [PATCH 5/5] added changes based on PR comments to the proposal --- docs/architecture/adr-006-trust-metric.md | 32 +++--- p2p/trust/trustmetric.go | 129 +++++++++++----------- p2p/trust/trustmetric_test.go | 14 +-- 3 files changed, 85 insertions(+), 90 deletions(-) diff --git a/docs/architecture/adr-006-trust-metric.md b/docs/architecture/adr-006-trust-metric.md index 961830cf9..ec8a0cce7 100644 --- a/docs/architecture/adr-006-trust-metric.md +++ b/docs/architecture/adr-006-trust-metric.md @@ -38,7 +38,7 @@ where *R*[*i*] denotes the raw trust value at time interval *i* (where *i* == 0 `H[i] = ` ![formula1](img/formula1.png "Weighted Sum Formula") -The weights can be chosen either optimistically or pessimistically. With the history value available, we can now finish calculating the integral value: +The weights can be chosen either optimistically or pessimistically. An optimistic weight creates larger weights for newer history data values, while the the pessimistic weight creates larger weights for time intervals with lower scores. The default weights used during the calculation of the history value are optimistic and calculated as *Wk* = 0.8^*k*, for time interval *k*. With the history value available, we can now finish calculating the integral value: ```math (2) Integral Value = b * H[i] @@ -49,13 +49,13 @@ Where *H*[*i*] denotes the history value at time interval *i* and *b* is the wei ```math D[i] = R[i] – H[i] -(3) Derivative Value = (c * D[i]) * D[i] +(3) Derivative Value = c(D[i]) * D[i] ``` -Where the value of *c* is selected based on the *D*[*i*] value relative to zero. With the three components brought together, our trust value equation is calculated as follows: +Where the value of *c* is selected based on the *D*[*i*] value relative to zero. The default selection process makes *c* equal to 0 unless *D*[*i*] is a negative value, in which case c is equal to 1. The result is that the maximum penalty is applied when current behavior is lower than previously experienced behavior. If the current behavior is better than the previously experienced behavior, then the Derivative Value has no impact on the trust value. With the three components brought together, our trust value equation is calculated as follows: ```math -TrustValue[i] = a * R[i] + b * H[i] + (c * D[i]) * D[i] +TrustValue[i] = a * R[i] + b * H[i] + c(D[i]) * D[i] ``` As a performance optimization that will keep the amount of raw interval data being saved to a reasonable size of *m*, while allowing us to represent 2^*m* - 1 history intervals, we can employ the fading memories technique that will trade space and time complexity for the precision of the history data values by summarizing larger quantities of less recent values. While our equation above attempts to access up to *maxH* (which can be 2^*m* - 1), we will map those requests down to *m* values using equation 4 below: @@ -99,17 +99,11 @@ func (tm *TrustMetric) Pause() {} // Stop tells the metric to stop recording data over time intervals func (tm *TrustMetric) Stop() {} -// BadEvent indicates that an undesirable event took place -func (tm *TrustMetric) BadEvent() {} +// BadEvents indicates that an undesirable event(s) took place +func (tm *TrustMetric) BadEvents(num int) {} -// AddBadEvents acknowledges multiple undesirable events -func (tm *TrustMetric) AddBadEvents(num int) {} - -// GoodEvent indicates that a desirable event took place -func (tm *TrustMetric) GoodEvent() {} - -// AddGoodEvents acknowledges multiple desirable events -func (tm *TrustMetric) AddGoodEvents(num int) {} +// GoodEvents indicates that a desirable event(s) took place +func (tm *TrustMetric) GoodEvents(num int) {} // TrustValue gets the dependable trust value; always between 0 and 1 func (tm *TrustMetric) TrustValue() float64 {} @@ -125,7 +119,7 @@ func NewMetric() *TrustMetric {} tm := NewMetric() -tm.BadEvent() +tm.BadEvents(1) score := tm.TrustScore() tm.Stop() @@ -170,9 +164,9 @@ config := TrustMetricConfig{ tm := NewMetricWithConfig(config) -tm.AddBadEvents(10) +tm.BadEvents(10) tm.Pause() -tm.GoodEvent() // becomes active again +tm.GoodEvents(1) // becomes active again ``` @@ -217,7 +211,7 @@ db := dbm.NewDB("trusthistory", "goleveldb", dirPathStr) tms := NewTrustMetricStore(db, DefaultConfig()) tm := tms.GetPeerTrustMetric(key) -tm.BadEvent() +tm.BadEvents(1) tms.PeerDisconnected(key) @@ -225,7 +219,7 @@ tms.PeerDisconnected(key) ## Status -Proposed. +Approved. ## Consequences diff --git a/p2p/trust/trustmetric.go b/p2p/trust/trustmetric.go index 39c24c247..84a11b1ca 100644 --- a/p2p/trust/trustmetric.go +++ b/p2p/trust/trustmetric.go @@ -30,9 +30,6 @@ type TrustMetricStore struct { // This configuration will be used when creating new TrustMetrics config TrustMetricConfig - - // This channel is used to stop the store go-routine - stop chan int } // NewTrustMetricStore returns a store that saves data to the DB @@ -42,7 +39,6 @@ func NewTrustMetricStore(db dbm.DB, tmc TrustMetricConfig) *TrustMetricStore { peerMetrics: make(map[string]*TrustMetric), db: db, config: tmc, - stop: make(chan int, 2), } tms.BaseService = *cmn.NewBaseService(nil, "TrustMetricStore", tms) @@ -57,14 +53,13 @@ func (tms *TrustMetricStore) OnStart() error { defer tms.mtx.Unlock() tms.loadFromDB() - go tms.periodicSave() + go tms.saveRoutine() return nil } // OnStop implements Service func (tms *TrustMetricStore) OnStop() { - // Stop the store periodic save go-routine - tms.stop <- 1 + tms.BaseService.OnStop() tms.mtx.Lock() defer tms.mtx.Unlock() @@ -76,7 +71,6 @@ func (tms *TrustMetricStore) OnStop() { // Make the final trust history data save tms.saveToDB() - tms.BaseService.OnStop() } // Size returns the number of entries in the trust metric store @@ -130,7 +124,7 @@ type peerHistoryJSON struct { History []float64 `json:"history"` } -// Loads the history data for the Peer identified by key from the store DB. +// 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 @@ -157,10 +151,21 @@ func (tms *TrustMetricStore) loadFromDB() bool { tm.numIntervals = p.NumIntervals // Restore the history and its current size if len(p.History) > tm.historyMaxSize { - p.History = p.History[:tm.historyMaxSize] + // Keep the history no larger than historyMaxSize + last := len(p.History) - tm.historyMaxSize + p.History = p.History[last:] } tm.history = p.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() // Load the peer trust metric into the store @@ -193,7 +198,7 @@ func (tms *TrustMetricStore) saveToDB() { } // Periodically saves the trust history data to the DB -func (tms *TrustMetricStore) periodicSave() { +func (tms *TrustMetricStore) saveRoutine() { t := time.NewTicker(defaultStorePeriodicSaveInterval) defer t.Stop() loop: @@ -203,7 +208,7 @@ loop: tms.mtx.Lock() tms.saveToDB() tms.mtx.Unlock() - case <-tms.stop: + case <-tms.Quit: break loop } } @@ -211,11 +216,22 @@ loop: //--------------------------------------------------------------------------------------- -// The number of event updates that can be sent on a single metric before blocking -const defaultUpdateChanCapacity = 10 +const ( + // The number of event updates that can be sent on a single metric before blocking + defaultUpdateChanCapacity = 10 + + // The number of trust value requests that can be made simultaneously before blocking + defaultRequestChanCapacity = 10 -// The number of trust value requests that can be made simultaneously before blocking -const defaultRequestChanCapacity = 10 + // 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 @@ -238,6 +254,12 @@ type TrustMetric struct { // 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 @@ -286,23 +308,13 @@ func (tm *TrustMetric) Stop() { tm.stop <- true } -// BadEvent indicates that an undesirable event took place -func (tm *TrustMetric) BadEvent() { - tm.update <- &updateBadGood{IsBad: true, Add: 1} -} - -// AddBadEvents acknowledges multiple undesirable events -func (tm *TrustMetric) AddBadEvents(num int) { +// BadEvents indicates that an undesirable event(s) took place +func (tm *TrustMetric) BadEvents(num int) { tm.update <- &updateBadGood{IsBad: true, Add: num} } -// GoodEvent indicates that a desirable event took place -func (tm *TrustMetric) GoodEvent() { - tm.update <- &updateBadGood{IsBad: false, Add: 1} -} - -// AddGoodEvents acknowledges multiple desirable events -func (tm *TrustMetric) AddGoodEvents(num int) { +// GoodEvents indicates that a desirable event(s) took place +func (tm *TrustMetric) GoodEvents(num int) { tm.update <- &updateBadGood{IsBad: false, Add: num} } @@ -316,10 +328,9 @@ func (tm *TrustMetric) TrustValue() float64 { // TrustScore gets a score based on the trust value always between 0 and 100 func (tm *TrustMetric) TrustScore() int { - resp := make(chan float64, 1) + score := tm.TrustValue() * 100 - tm.trustValue <- &reqTrustValue{Resp: resp} - return int(math.Floor(<-resp * 100)) + return int(math.Floor(score)) } // TrustMetricConfig - Configures the weight functions and time intervals for the metric @@ -373,7 +384,7 @@ func NewMetricWithConfig(tmc TrustMetricConfig) *TrustMetric { // Setup the channels tm.update = make(chan *updateBadGood, defaultUpdateChanCapacity) tm.trustValue = make(chan *reqTrustValue, defaultRequestChanCapacity) - tm.stop = make(chan bool, 2) + tm.stop = make(chan bool, 1) go tm.processRequests() return tm @@ -413,12 +424,11 @@ func (tm *TrustMetric) derivativeValue() float64 { // Strengthens the derivative component when the change is negative func (tm *TrustMetric) weightedDerivative() float64 { - var weight float64 + var weight float64 = defaultDerivativeGamma1 d := tm.derivativeValue() - if d < 0 { - weight = 1.0 + weight = defaultDerivativeGamma2 } return weight * d } @@ -431,9 +441,10 @@ func (tm *TrustMetric) updateFadedMemory() { return } - first := tm.historySize - 1 + end := tm.historySize - 1 // Keep the most recent history element - for count, i := 1, first-1; count < tm.historySize; count, i = count+1, i-1 { + 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 @@ -443,7 +454,10 @@ func (tm *TrustMetric) updateFadedMemory() { // Map the interval value down to an offset from the beginning of history func intervalToHistoryOffset(interval int) int { - return int(math.Floor(math.Log(float64(interval)) / math.Log(2))) + // 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 @@ -461,37 +475,21 @@ func (tm *TrustMetric) fadedMemoryValue(interval int) float64 { // Calculates the integral (history) component of the trust value func (tm *TrustMetric) calcHistoryValue() float64 { - var wk []float64 - - // Create the weights. - hlen := tm.numIntervals - for i := 0; i < hlen; i++ { - x := math.Pow(.8, float64(i+1)) // Optimistic weight - wk = append(wk, x) - } + var hv float64 - var wsum float64 - // Calculate the sum of the weights - for _, v := range wk { - wsum += v + for i := 0; i < tm.numIntervals; i++ { + hv += tm.fadedMemoryValue(i) * tm.historyWeights[i] } - var hv float64 - // Calculate the history value - for i := 0; i < hlen; i++ { - weight := wk[i] / wsum - hv += tm.fadedMemoryValue(i) * weight - } - return hv + return hv / tm.historyWeightSum } // Calculates the current score for good/bad experiences func (tm *TrustMetric) proportionalValue() float64 { value := 1.0 - // Bad events are worth more in the calculation of our score - total := tm.good + math.Pow(tm.bad, 2) - if tm.bad > 0 || tm.good > 0 { + total := tm.good + tm.bad + if total > 0 { value = tm.good / total } return value @@ -545,14 +543,17 @@ loop: if tm.historySize < tm.historyMaxSize { tm.historySize++ } else { - last := len(tm.history) - tm.historyMaxSize - // 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 diff --git a/p2p/trust/trustmetric_test.go b/p2p/trust/trustmetric_test.go index 626ca3bd5..56441c721 100644 --- a/p2p/trust/trustmetric_test.go +++ b/p2p/trust/trustmetric_test.go @@ -45,8 +45,8 @@ func TestTrustMetricStoreSaveLoad(t *testing.T) { key := fmt.Sprintf("peer_%d", i) tm := store.GetPeerTrustMetric(key) - tm.AddBadEvents(10) - tm.GoodEvent() + tm.BadEvents(10) + tm.GoodEvents(1) } // Check that we have 100 entries and save @@ -134,10 +134,10 @@ func TestTrustMetricStorePeerScore(t *testing.T) { assert.Equal(t, 100, first) // Add some undesirable events and disconnect - tm.BadEvent() + tm.BadEvents(1) first = tm.TrustScore() assert.NotEqual(t, 100, first) - tm.AddBadEvents(10) + tm.BadEvents(10) second := tm.TrustScore() if second > first { @@ -155,12 +155,12 @@ func TestTrustMetricScores(t *testing.T) { tm := NewMetric() // Perfect score - tm.GoodEvent() + tm.GoodEvents(1) score := tm.TrustScore() assert.Equal(t, 100, score) // Less than perfect score - tm.AddBadEvents(10) + tm.BadEvents(10) score = tm.TrustScore() assert.NotEqual(t, 100, score) tm.Stop() @@ -216,7 +216,7 @@ func TestTrustMetricStopPause(t *testing.T) { assert.Equal(t, first, tm.numIntervals) // Get the trust metric activated again - tm.AddGoodEvents(5) + tm.GoodEvents(5) // Allow some time intervals to pass and stop time.Sleep(50 * time.Millisecond) tm.Stop()