-
Notifications
You must be signed in to change notification settings - Fork 7
Add activity tracking to entropy generator #194
Changes from 5 commits
97ddbd9
6a67020
5e382e4
293257c
8512885
0751104
de04e18
e144799
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package beacon | ||
|
||
import ( | ||
"container/list" | ||
"fmt" | ||
"runtime/debug" | ||
"sync" | ||
|
@@ -10,6 +11,7 @@ import ( | |
dbm "github.com/tendermint/tm-db" | ||
|
||
cfg "github.com/tendermint/tendermint/config" | ||
"github.com/tendermint/tendermint/crypto" | ||
"github.com/tendermint/tendermint/crypto/tmhash" | ||
tmevents "github.com/tendermint/tendermint/libs/events" | ||
"github.com/tendermint/tendermint/libs/log" | ||
|
@@ -25,6 +27,12 @@ const ( | |
maxNextAeons = 5 | ||
) | ||
|
||
// interface to the evidence pool | ||
type evidencePool interface { | ||
AddEvidence(types.Evidence) error | ||
PendingEvidence(int64) []types.Evidence | ||
} | ||
|
||
// EntropyGenerator holds DKG keys for computing entropy and computes entropy shares | ||
// and entropy for dispatching along channel. Entropy generation is blocked by arrival of keys for the | ||
// keys for the current block height from the dkg - including for trivial entropy periods, for which the | ||
|
@@ -58,6 +66,12 @@ type EntropyGenerator struct { | |
metrics *Metrics | ||
creatingEntropyAtHeight int64 | ||
creatingEntropyAtTimeMs time.Time | ||
|
||
// add evidence to the pool when it's detected | ||
stateDB dbm.DB | ||
evpool evidencePool | ||
aeonEntropyParams *types.EntropyParams // Inactivity params for this aeon | ||
activityTracking map[uint]*list.List // Record of validators | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be a pointer? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is slightly nicer when iterating through the map as you can edit the list directly rather than having to access it by indexing into the map. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
} | ||
|
||
func (entropyGenerator *EntropyGenerator) AttachMetrics(metrics *Metrics) { | ||
|
@@ -70,7 +84,8 @@ func (entropyGenerator *EntropyGenerator) AttachMetrics(metrics *Metrics) { | |
} | ||
|
||
// NewEntropyGenerator creates new entropy generator with validator information | ||
func NewEntropyGenerator(bConfig *cfg.BaseConfig, beaconConfig *cfg.BeaconConfig, blockHeight int64) *EntropyGenerator { | ||
func NewEntropyGenerator(bConfig *cfg.BaseConfig, beaconConfig *cfg.BeaconConfig, blockHeight int64, | ||
evpool evidencePool, stateDB dbm.DB) *EntropyGenerator { | ||
if bConfig == nil || beaconConfig == nil { | ||
panic(fmt.Errorf("NewEntropyGenerator: baseConfig/beaconConfig can not be nil")) | ||
} | ||
|
@@ -84,6 +99,8 @@ func NewEntropyGenerator(bConfig *cfg.BaseConfig, beaconConfig *cfg.BeaconConfig | |
evsw: tmevents.NewEventSwitch(), | ||
quit: make(chan struct{}), | ||
metrics: NopMetrics(), | ||
evpool: evpool, | ||
stateDB: stateDB, | ||
} | ||
|
||
es.BaseService = *service.NewBaseService(nil, "EntropyGenerator", es) | ||
|
@@ -222,6 +239,7 @@ func (entropyGenerator *EntropyGenerator) SetNextAeonDetails(aeon *aeonDetails) | |
|
||
// If over max number of keys pop of the oldest one | ||
if len(entropyGenerator.nextAeons) > maxNextAeons { | ||
entropyGenerator.nextAeons[0] = nil | ||
entropyGenerator.nextAeons = entropyGenerator.nextAeons[1:len(entropyGenerator.nextAeons)] | ||
} | ||
entropyGenerator.nextAeons = append(entropyGenerator.nextAeons, aeon) | ||
|
@@ -245,6 +263,7 @@ func (entropyGenerator *EntropyGenerator) trimNextAeons() { | |
if len(entropyGenerator.nextAeons) == 1 { | ||
entropyGenerator.nextAeons = make([]*aeonDetails, 0) | ||
} else { | ||
entropyGenerator.nextAeons[0] = nil | ||
entropyGenerator.nextAeons = entropyGenerator.nextAeons[1:len(entropyGenerator.nextAeons)] | ||
} | ||
} else { | ||
|
@@ -302,6 +321,8 @@ func (entropyGenerator *EntropyGenerator) changeKeys() (didChangeKeys bool) { | |
entropyGenerator.Logger.Info("changeKeys: Loaded new keys", "blockHeight", entropyGenerator.lastBlockHeight, | ||
"start", entropyGenerator.aeon.Start) | ||
didChangeKeys = true | ||
|
||
entropyGenerator.resetActivityTracking() | ||
} | ||
|
||
// If lastComputedEntropyHeight is not set then set it is equal to group public key (should | ||
|
@@ -552,6 +573,9 @@ OUTER_LOOP: | |
entropyGenerator.metrics.EntropyGenerating.Set(0.0) | ||
} | ||
|
||
// Check tracking information | ||
entropyGenerator.updateActivityTracking(entropyToSend) | ||
|
||
// Clean out old entropy shares and computed entropy | ||
entropyGenerator.flushOldEntropy() | ||
} | ||
|
@@ -634,6 +658,106 @@ func (entropyGenerator *EntropyGenerator) flushOldEntropy() { | |
} | ||
} | ||
|
||
// Resets the activity tracking when aeon keys have changed over. Only track signature shares if in qual, | ||
// but do not track own activity | ||
func (entropyGenerator *EntropyGenerator) resetActivityTracking() { | ||
if entropyGenerator.aeon.aeonExecUnit == nil || !entropyGenerator.aeon.aeonExecUnit.CanSign() { | ||
return | ||
} | ||
|
||
entropyGenerator.activityTracking = make(map[uint]*list.List) | ||
ownIndex := -1 | ||
pubKey, err := entropyGenerator.aeon.privValidator.GetPubKey() | ||
if err == nil { | ||
ownIndex, _ = entropyGenerator.aeon.validators.GetByAddress(pubKey.Address()) | ||
} | ||
for i := 0; i < entropyGenerator.aeon.validators.Size(); i++ { | ||
valIndex := uint(i) | ||
if i != ownIndex && entropyGenerator.aeon.aeonExecUnit.InQual(valIndex) { | ||
entropyGenerator.activityTracking[valIndex] = list.New() | ||
} | ||
} | ||
|
||
// Fetch parameters at aeon dkg validator height so that they remain constant during the entire aeon | ||
paramHeight := entropyGenerator.aeon.validatorHeight | ||
newParams, err := sm.LoadConsensusParams(entropyGenerator.stateDB, paramHeight) | ||
if err != nil { | ||
entropyGenerator.Logger.Error("resetActivityTracking: error fetching consensus params", "height", paramHeight, | ||
"err", err) | ||
return | ||
} | ||
entropyGenerator.aeonEntropyParams = &newParams.Entropy | ||
} | ||
|
||
// Updates tracking information with signature shares from most recent height. Calculates signature shares | ||
// received in the last window and creates evidence if not enough were received | ||
func (entropyGenerator *EntropyGenerator) updateActivityTracking(entropy *types.ChannelEntropy) { | ||
if entropyGenerator.aeon.aeonExecUnit == nil || !entropyGenerator.aeon.aeonExecUnit.CanSign() { | ||
return | ||
} | ||
if entropyGenerator.aeonEntropyParams == nil { | ||
return | ||
} | ||
|
||
entropyGenerator.mtx.Lock() | ||
defer entropyGenerator.mtx.Unlock() | ||
|
||
for valIndex, activity := range entropyGenerator.activityTracking { | ||
// If entropy is not enabled then should have received no signature shares from anyone this | ||
// height and append true to all validators activity tracking | ||
if _, haveShare := entropyGenerator.entropyShares[entropy.Height][valIndex]; !entropy.Enabled || haveShare { | ||
activity.PushBack(1) | ||
} else { | ||
activity.PushBack(0) | ||
} | ||
|
||
// Return if we have not got records for InactivityWindowSize blocks | ||
if activity.Len() < int(entropyGenerator.aeonEntropyParams.InactivityWindowSize) { | ||
continue | ||
} | ||
|
||
// Trim to sliding window size if too large by removing oldest elements | ||
for activity.Len() > int(entropyGenerator.aeonEntropyParams.InactivityWindowSize) { | ||
activity.Remove(activity.Front()) | ||
} | ||
|
||
// Measure inactivity | ||
sigShareCount := 0 | ||
for e := activity.Front(); e != nil; e = e.Next() { | ||
sigShareCount += e.Value.(int) | ||
} | ||
|
||
threshold := float64(entropyGenerator.aeonEntropyParams.RequiredActivityPercentage*entropyGenerator.aeonEntropyParams.InactivityWindowSize) * 0.01 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might break this up into multiple lines, and also make it a log info for when there is evidence submitted against you - in my mind if something unexpected is happening such as slashing there should be a log so you notice it asap. I found that in the sdk tests it was confusing when you would be suddenly jailed with no indication why, if it had printed 'validator jailed for inactivity' that would have been helpful. |
||
if sigShareCount < int(threshold) { | ||
// Create evidence and submit to evpool | ||
defAddress, _ := entropyGenerator.aeon.validators.GetByIndex(int(valIndex)) | ||
pubKey, err := entropyGenerator.aeon.privValidator.GetPubKey() | ||
if err != nil { | ||
entropyGenerator.Logger.Error("updateActivityTracking: error getting pub key", "err", err) | ||
continue | ||
} | ||
evidence := types.NewBeaconInactivityEvidence(entropy.Height, defAddress, pubKey.Address(), | ||
entropyGenerator.aeon.Start) | ||
sig, err := entropyGenerator.aeon.privValidator.SignEvidence(entropyGenerator.baseConfig.ChainID(), evidence) | ||
if err != nil { | ||
entropyGenerator.Logger.Error("updateActivityTracking: error signing evidence", "err", err) | ||
continue | ||
} | ||
evidence.ComplainantSignature = sig | ||
|
||
entropyGenerator.Logger.Debug("Add evidence for inactivity", "height", entropy.Height, "validator", crypto.AddressHash(defAddress)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would consider making this a log Info. When doing the SDK tests I would have appreciated if the logs said something to the effect of 'jailing/slashing validator for inactivity' when it happened so you aren't wondering why it is suddenly jailed. In my mind there should be logs by default if something unexpected happens There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure thing. The slashing module actually does have Info logs when it jails a validator. Not sure why they weren't printing out on the tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh - perhaps I missed them. I guess the logs rather than being here could be in the SDK actually, since that would be when action actually happened |
||
entropyGenerator.evpool.AddEvidence(evidence) | ||
|
||
// Paid price for not contributing in this window so now convert the entire window to 1s in order | ||
// for validator not be slashed again for this window | ||
entropyGenerator.activityTracking[valIndex] = list.New() | ||
for i := int64(0); i < entropyGenerator.aeonEntropyParams.InactivityWindowSize; i++ { | ||
entropyGenerator.activityTracking[valIndex].PushBack(1) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func (entropyGenerator *EntropyGenerator) isSigningEntropy() bool { | ||
entropyGenerator.mtx.RLock() | ||
defer entropyGenerator.mtx.RUnlock() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it need to be a pointer to a list?