Skip to content
This repository has been archived by the owner on Aug 24, 2022. It is now read-only.

Add activity tracking to entropy generator #194

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions beacon/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,10 @@ func randBeaconAndConsensusNet(nValidators int, testName string, withConsensus b
pubKey, _ := privVals[i].GetPubKey()
index, _ := state.Validators.GetByAddress(pubKey.Address())
blockStores[i] = store.NewBlockStore(stateDB)
evpool := sm.MockEvidencePool{}

aeonDetails, _ := newAeonDetails(privVals[i], 1, 1, state.Validators, aeonExecUnits[index], 1, 9)
entropyGenerators[i] = NewEntropyGenerator(&thisConfig.BaseConfig, thisConfig.Beacon, 0)
entropyGenerators[i] = NewEntropyGenerator(&thisConfig.BaseConfig, thisConfig.Beacon, 0, evpool, stateDB)
entropyGenerators[i].SetLogger(logger)
entropyGenerators[i].SetLastComputedEntropy(0, state.LastComputedEntropy)
entropyGenerators[i].SetNextAeonDetails(aeonDetails)
Expand Down Expand Up @@ -171,9 +172,10 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G
sort.Sort(types.PrivValidatorsByAddress(privValidators))

return &types.GenesisDoc{
GenesisTime: tmtime.Now(),
ChainID: config.ChainID(),
Validators: validators,
Entropy: "Fetch.ai Test Genesis Entropy",
GenesisTime: tmtime.Now(),
ChainID: config.ChainID(),
ConsensusParams: types.DefaultConsensusParams(),
Validators: validators,
Entropy: "Fetch.ai Test Genesis Entropy",
}, privValidators
}
126 changes: 125 additions & 1 deletion beacon/entropy_generator.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package beacon

import (
"container/list"
"fmt"
"runtime/debug"
"sync"
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Copy link
Contributor

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a pointer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

func (entropyGenerator *EntropyGenerator) AttachMetrics(metrics *Metrics) {
Expand All @@ -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"))
}
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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))
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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()
Expand Down
Loading