diff --git a/beacon/common_test.go b/beacon/common_test.go index 026c56fe4..8fc271b7c 100644 --- a/beacon/common_test.go +++ b/beacon/common_test.go @@ -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(state.ChainID, &thisConfig.BaseConfig, thisConfig.Beacon, 0, evpool, stateDB) entropyGenerators[i].SetLogger(logger) entropyGenerators[i].SetLastComputedEntropy(0, state.LastComputedEntropy) entropyGenerators[i].SetNextAeonDetails(aeonDetails) @@ -170,10 +171,15 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G } sort.Sort(types.PrivValidatorsByAddress(privValidators)) + // Make inactivity window smaller for tests + params := types.DefaultConsensusParams() + params.Entropy.InactivityWindowSize = 50 + return &types.GenesisDoc{ - GenesisTime: tmtime.Now(), - ChainID: config.ChainID(), - Validators: validators, - Entropy: "Fetch.ai Test Genesis Entropy", + GenesisTime: tmtime.Now(), + ChainID: config.ChainID(), + ConsensusParams: params, + Validators: validators, + Entropy: "Fetch.ai Test Genesis Entropy", }, privValidators } diff --git a/beacon/entropy_generator.go b/beacon/entropy_generator.go index 6dc5975aa..8dd8e9fb4 100644 --- a/beacon/entropy_generator.go +++ b/beacon/entropy_generator.go @@ -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 @@ -44,6 +52,7 @@ type EntropyGenerator struct { nextAeons []*aeonDetails aeon *aeonDetails + chainID string baseConfig *cfg.BaseConfig beaconConfig *cfg.BeaconConfig @@ -58,6 +67,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 } func (entropyGenerator *EntropyGenerator) AttachMetrics(metrics *Metrics) { @@ -70,7 +85,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(chainID string, 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")) } @@ -79,11 +95,14 @@ func NewEntropyGenerator(bConfig *cfg.BaseConfig, beaconConfig *cfg.BeaconConfig lastBlockHeight: blockHeight, lastComputedEntropyHeight: -1, // value is invalid and requires last entropy to be set entropyComputed: make(map[int64]types.ThresholdSignature), + chainID: chainID, baseConfig: bConfig, beaconConfig: beaconConfig, evsw: tmevents.NewEventSwitch(), quit: make(chan struct{}), metrics: NopMetrics(), + evpool: evpool, + stateDB: stateDB, } es.BaseService = *service.NewBaseService(nil, "EntropyGenerator", es) @@ -222,6 +241,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 +265,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 +323,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 @@ -357,7 +380,7 @@ func (entropyGenerator *EntropyGenerator) applyEntropyShare(share *types.Entropy } // Verify signature on message - verifySig := validator.PubKey.VerifyBytes(share.SignBytes(entropyGenerator.baseConfig.ChainID()), share.Signature) + verifySig := validator.PubKey.VerifyBytes(share.SignBytes(entropyGenerator.chainID), share.Signature) if !verifySig { entropyGenerator.Logger.Error("applyEntropyShare: invalid validator signature", "validator", share.SignerAddress, "index", index) return @@ -472,7 +495,7 @@ func (entropyGenerator *EntropyGenerator) sign() { SignatureShare: signature, } // Sign message - err = entropyGenerator.aeon.privValidator.SignEntropy(entropyGenerator.baseConfig.ChainID(), &share) + err = entropyGenerator.aeon.privValidator.SignEntropy(entropyGenerator.chainID, &share) if err != nil { entropyGenerator.Logger.Error(err.Error()) return @@ -552,6 +575,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 +660,111 @@ 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) + } + + requiredFraction := float64(entropyGenerator.aeonEntropyParams.RequiredActivityPercentage) * 0.01 + threshold := int(requiredFraction * float64(entropyGenerator.aeonEntropyParams.InactivityWindowSize)) + if sigShareCount < 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.chainID, evidence) + if err != nil { + entropyGenerator.Logger.Error("updateActivityTracking: error signing evidence", "err", err) + continue + } + evidence.ComplainantSignature = sig + + entropyGenerator.Logger.Info("Add evidence for inactivity", "height", entropy.Height, "validator", crypto.AddressHash(defAddress), + "shareCount", sigShareCount, "threshold", threshold, "windowSize", entropyGenerator.aeonEntropyParams.InactivityWindowSize) + err = entropyGenerator.evpool.AddEvidence(evidence) + if err != nil { + entropyGenerator.Logger.Error("updateActivityTracking: error adding evidence", "err", err) + } + + // 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() diff --git a/beacon/entropy_generator_test.go b/beacon/entropy_generator_test.go index 2d6492a3c..145ce4772 100644 --- a/beacon/entropy_generator_test.go +++ b/beacon/entropy_generator_test.go @@ -16,6 +16,26 @@ import ( "github.com/tendermint/tendermint/types" ) +// Mock evidence pool for inactivity tracking +type mockEvidencePool struct { + receivedEvidence []types.Evidence +} + +func newMockEvidencePool() *mockEvidencePool { + return &mockEvidencePool{ + receivedEvidence: make([]types.Evidence, 0), + } +} + +func (mep *mockEvidencePool) AddEvidence(ev types.Evidence) error { + mep.receivedEvidence = append(mep.receivedEvidence, ev) + return nil +} + +func (mep *mockEvidencePool) PendingEvidence(int64) []types.Evidence { + return mep.receivedEvidence +} + func TestEntropyGeneratorStart(t *testing.T) { testCases := []struct { testName string @@ -35,7 +55,7 @@ func TestEntropyGeneratorStart(t *testing.T) { } for _, tc := range testCases { t.Run(tc.testName, func(t *testing.T) { - newGen := testEntropyGenerator() + newGen := testEntropyGenerator("TestChain") tc.setup(newGen) assert.NotPanics(t, func() { newGen.Start() @@ -45,7 +65,7 @@ func TestEntropyGeneratorStart(t *testing.T) { } func TestEntropyGeneratorSetAeon(t *testing.T) { - newGen := testEntropyGenerator() + newGen := testEntropyGenerator("TestChain") // Set be on the end of first aeon lastBlockHeight := int64(99) newGen.setLastBlockHeight(lastBlockHeight) @@ -76,7 +96,7 @@ func TestEntropyGeneratorNonValidator(t *testing.T) { nValidators := 4 state, privVals := groupTestSetup(nValidators) - newGen := testEntropyGen(state.Validators, nil, -1) + newGen := testEntropyGen(state, nil, -1) // Does not panic if can not sign assert.NotPanics(t, func() { @@ -90,7 +110,7 @@ func TestEntropyGeneratorNonValidator(t *testing.T) { privVal := privVals[i] pubKey, _ := privVal.GetPubKey() index, _ := state.Validators.GetByAddress(pubKey.Address()) - tempGen := testEntropyGen(state.Validators, privVal, index) + tempGen := testEntropyGen(state, privVal, index) tempGen.sign() share := tempGen.entropyShares[1][uint(index)] @@ -106,7 +126,7 @@ func TestEntropyGeneratorSign(t *testing.T) { pubKey, _ := privVals[0].GetPubKey() index, _ := state.Validators.GetByAddress(pubKey.Address()) - newGen := testEntropyGen(state.Validators, privVals[0], index) + newGen := testEntropyGen(state, privVals[0], index) newGen.SetLastComputedEntropy(2, []byte("Test Entropy")) newGen.setLastBlockHeight(2) @@ -126,7 +146,7 @@ func TestEntropyGeneratorApplyShare(t *testing.T) { state, privVals := groupTestSetup(nValidators) // Set up non-validator - newGen := testEntropyGen(state.Validators, nil, -1) + newGen := testEntropyGen(state, nil, -1) newGen.SetLastComputedEntropy(1, []byte("Test Entropy")) newGen.setLastBlockHeight(1) @@ -150,7 +170,7 @@ func TestEntropyGeneratorApplyShare(t *testing.T) { t.Run("applyShare old height", func(t *testing.T) { pubKey, _ := privVals[0].GetPubKey() index, _ := state.Validators.GetByAddress(pubKey.Address()) - otherGen := testEntropyGen(state.Validators, privVals[0], index) + otherGen := testEntropyGen(state, privVals[0], index) otherGen.SetLastComputedEntropy(1, []byte("Test Entropy")) otherGen.setLastBlockHeight(1) @@ -163,7 +183,7 @@ func TestEntropyGeneratorApplyShare(t *testing.T) { t.Run("applyShare height far ahead", func(t *testing.T) { pubKey, _ := privVals[0].GetPubKey() index, _ := state.Validators.GetByAddress(pubKey.Address()) - otherGen := testEntropyGen(state.Validators, privVals[0], index) + otherGen := testEntropyGen(state, privVals[0], index) otherGen.SetLastComputedEntropy(3, []byte("Test Entropy")) otherGen.setLastBlockHeight(3) @@ -195,7 +215,7 @@ func TestEntropyGeneratorApplyShare(t *testing.T) { t.Run("applyShare invalid validator signature", func(t *testing.T) { pubKey, _ := privVals[0].GetPubKey() index, _ := state.Validators.GetByAddress(pubKey.Address()) - otherGen := testEntropyGen(state.Validators, privVals[0], index) + otherGen := testEntropyGen(state, privVals[0], index) otherGen.SetLastComputedEntropy(1, []byte("Test Entropy")) otherGen.setLastBlockHeight(1) @@ -210,7 +230,7 @@ func TestEntropyGeneratorApplyShare(t *testing.T) { t.Run("applyShare correct", func(t *testing.T) { pubKey, _ := privVals[0].GetPubKey() index, _ := state.Validators.GetByAddress(pubKey.Address()) - otherGen := testEntropyGen(state.Validators, privVals[0], index) + otherGen := testEntropyGen(state, privVals[0], index) otherGen.SetLastComputedEntropy(1, []byte("Test Entropy")) otherGen.setLastBlockHeight(1) @@ -225,7 +245,7 @@ func TestEntropyGeneratorApplyShare(t *testing.T) { func TestEntropyGeneratorFlush(t *testing.T) { state, privVal := groupTestSetup(1) - newGen := testEntropyGenerator() + newGen := testEntropyGenerator(state.ChainID) newGen.SetLogger(log.TestingLogger()) aeonExecUnit := testAeonFromFile("test_keys/single_validator.txt") @@ -247,7 +267,7 @@ func TestEntropyGeneratorApplyComputedEntropy(t *testing.T) { state, privVals := groupTestSetup(nValidators) // Set up non-validator - newGen := testEntropyGen(state.Validators, nil, -1) + newGen := testEntropyGen(state, nil, -1) newGen.SetLastComputedEntropy(1, []byte("Test Entropy")) newGen.setLastBlockHeight(1) newGen.Start() @@ -263,7 +283,7 @@ func TestEntropyGeneratorApplyComputedEntropy(t *testing.T) { t.Run("applyEntropy invalid entropy", func(t *testing.T) { pubKey, _ := privVals[0].GetPubKey() index, _ := state.Validators.GetByAddress(pubKey.Address()) - otherGen := testEntropyGen(state.Validators, privVals[0], index) + otherGen := testEntropyGen(state, privVals[0], index) otherGen.SetLastComputedEntropy(1, []byte("Test Entropy")) otherGen.setLastBlockHeight(1) @@ -275,7 +295,7 @@ func TestEntropyGeneratorApplyComputedEntropy(t *testing.T) { t.Run("applyEntropy correct", func(t *testing.T) { pubKey, _ := privVals[0].GetPubKey() index, _ := state.Validators.GetByAddress(pubKey.Address()) - otherGen := testEntropyGen(state.Validators, privVals[0], index) + otherGen := testEntropyGen(state, privVals[0], index) otherGen.SetLastComputedEntropy(1, []byte("Test Entropy")) otherGen.setLastBlockHeight(1) otherGen.Start() @@ -283,7 +303,7 @@ func TestEntropyGeneratorApplyComputedEntropy(t *testing.T) { for _, val := range privVals { pubKey, _ := val.GetPubKey() tempIndex, _ := state.Validators.GetByAddress(pubKey.Address()) - tempGen := testEntropyGen(state.Validators, val, tempIndex) + tempGen := testEntropyGen(state, val, tempIndex) tempGen.SetLastComputedEntropy(1, []byte("Test Entropy")) tempGen.setLastBlockHeight(1) @@ -299,7 +319,7 @@ func TestEntropyGeneratorApplyComputedEntropy(t *testing.T) { } func TestEntropyGeneratorChangeKeys(t *testing.T) { - newGen := testEntropyGenerator() + newGen := testEntropyGenerator("TestChain") newGen.SetLogger(log.TestingLogger()) newGen.SetNextAeonDetails(keylessAeonDetails(0, 4)) @@ -314,6 +334,91 @@ func TestEntropyGeneratorChangeKeys(t *testing.T) { assert.Eventually(t, func() bool { return newGen.isSigningEntropy() }, time.Second, 100*time.Millisecond) } +func TestEntropyResetActivityTracking(t *testing.T) { + nValidators := 4 + state, privVals := groupTestSetup(nValidators) + + testCases := []struct { + testName string + privVal types.PrivValidator + mapNil bool + activityTrackingSize int + }{ + {"Sentry", nil, true, 0}, + {"Validator", privVals[0], false, 3}, + } + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + index := -1 + if tc.privVal != nil { + pubKey, _ := tc.privVal.GetPubKey() + index, _ = state.Validators.GetByAddress(pubKey.Address()) + } + newGen := testEntropyGen(state, tc.privVal, index) + + assert.Equal(t, tc.mapNil, newGen.activityTracking == nil) + if !tc.mapNil { + assert.Equal(t, tc.activityTrackingSize, len(newGen.activityTracking)) + _, haveIndex := newGen.activityTracking[uint(index)] + assert.False(t, haveIndex) + assert.True(t, newGen.aeonEntropyParams != nil) + } + }) + } +} + +func TestEntropyActivityTracking(t *testing.T) { + nValidators := 4 + state, privVals := groupTestSetup(nValidators) + entropyParams := state.ConsensusParams.Entropy + windowSize := entropyParams.InactivityWindowSize + threshold := int64(float64(entropyParams.RequiredActivityPercentage*windowSize) * 0.01) + + // Set up validator + pubKey, _ := privVals[0].GetPubKey() + index, _ := state.Validators.GetByAddress(pubKey.Address()) + + testCases := []struct { + testName string + sharesReceived int64 + entropyHeight int64 + entropyEnabled bool + pendingEvidence int + }{ + {"Less than window size", 10, 10, true, 0}, + {"Slash all but one", threshold - 1, windowSize, false, 2}, + {"Slash everyone", threshold - 1, windowSize, true, 3}, + {"Does not double slash", threshold + 1, windowSize + 1, true, 2}, + {"Slash new misbehaviour", threshold, windowSize + 1, true, 3}, + } + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + newGen := testEntropyGen(state, privVals[0], index) + + otherIndex := uint((index + 1) % nValidators) + for i := int64(0); i < tc.entropyHeight; i++ { + if i < tc.sharesReceived { + newGen.entropyShares[i+1] = make(map[uint]types.EntropyShare) + newGen.entropyShares[i+1][otherIndex] = types.EntropyShare{} + } + enabled := true + if i == tc.entropyHeight-1 { + enabled = tc.entropyEnabled + } + newGen.updateActivityTracking(types.NewChannelEntropy(i+1, types.BlockEntropy{}, enabled, nil)) + } + + evidence := newGen.evpool.PendingEvidence(0) + assert.Equal(t, tc.pendingEvidence, len(evidence)) + for _, ev := range evidence { + _, val := state.Validators.GetByAddress(ev.Address()) + assert.Nil(t, ev.Verify(state.ChainID, val.PubKey)) + } + + }) + } +} + func groupTestSetup(nValidators int) (sm.State, []types.PrivValidator) { genDoc, privVals := randGenesisDoc(nValidators, false, 30) stateDB := dbm.NewMemDB() // each state needs its own db @@ -321,22 +426,24 @@ func groupTestSetup(nValidators int) (sm.State, []types.PrivValidator) { return state, privVals } -func testEntropyGen(validators *types.ValidatorSet, privVal types.PrivValidator, index int) *EntropyGenerator { - newGen := testEntropyGenerator() +func testEntropyGen(state sm.State, privVal types.PrivValidator, index int) *EntropyGenerator { + newGen := testEntropyGenerator(state.ChainID) newGen.SetLogger(log.TestingLogger()) + sm.SaveState(newGen.stateDB, state) aeonExecUnit := testAeonFromFile("test_keys/non_validator.txt") if index >= 0 { aeonExecUnit = testAeonFromFile("test_keys/validator_" + strconv.Itoa(int(index)) + "_of_4.txt") } - aeonDetails, _ := newAeonDetails(privVal, 1, 1, validators, aeonExecUnit, 1, 50) + aeonDetails, _ := newAeonDetails(privVal, 1, 1, state.Validators, aeonExecUnit, 1, 50) newGen.SetNextAeonDetails(aeonDetails) newGen.SetLastComputedEntropy(0, []byte("Test Entropy")) newGen.changeKeys() return newGen } -func testEntropyGenerator() *EntropyGenerator { +func testEntropyGenerator(chainID string) *EntropyGenerator { config := cfg.ResetTestRoot("entropy_generator_test") - return NewEntropyGenerator(&config.BaseConfig, config.Beacon, 0) + stateDB := dbm.NewMemDB() // each state needs its own db + return NewEntropyGenerator(chainID, &config.BaseConfig, config.Beacon, 0, newMockEvidencePool(), stateDB) } diff --git a/evidence/pool.go b/evidence/pool.go index 68967ede1..8afb78114 100644 --- a/evidence/pool.go +++ b/evidence/pool.go @@ -108,7 +108,7 @@ func (evpool *Pool) AddEvidence(evidence types.Evidence) error { // fetch the validator and return its voting power as its priority // TODO: something better ? - valset, err := sm.LoadValidators(evpool.stateDB, evidence.Height()) + valset, err := sm.LoadValidators(evpool.stateDB, evidence.ValidatorHeight()) if err != nil { return err } diff --git a/node/node.go b/node/node.go index 4123847e7..3a822aa84 100644 --- a/node/node.go +++ b/node/node.go @@ -571,10 +571,11 @@ func createBeaconReactor( beaconLogger log.Logger, fastSync bool, blockStore sm.BlockStore, dkgRunner *beacon.DKGRunner, - db dbm.DB) (chan types.ChannelEntropy, *beacon.EntropyGenerator, *beacon.Reactor, error) { + db dbm.DB, + evpool *evidence.Pool) (chan types.ChannelEntropy, *beacon.EntropyGenerator, *beacon.Reactor, error) { beacon.InitialiseMcl() - entropyGenerator := beacon.NewEntropyGenerator(&config.BaseConfig, config.Beacon, state.LastBlockHeight) + entropyGenerator := beacon.NewEntropyGenerator(state.ChainID, &config.BaseConfig, config.Beacon, state.LastBlockHeight, evpool, db) entropyChannel := make(chan types.ChannelEntropy, config.Beacon.EntropyChannelCapacity) entropyGenerator.SetLogger(beaconLogger) entropyGenerator.SetEntropyChannel(entropyChannel) @@ -763,7 +764,7 @@ func NewNode(config *cfg.Config, // Make BeaconReactor beaconLogger := logger.With("module", "beacon") entropyChannel, entropyGenerator, beaconReactor, err := createBeaconReactor(config, state, privValidator, - beaconLogger, fastSync, blockStore, dkgRunner, stateDB) + beaconLogger, fastSync, blockStore, dkgRunner, stateDB, evidencePool) if err != nil { return nil, errors.Wrap(err, "could not load aeon keys from file") diff --git a/state/execution.go b/state/execution.go index 6238af96c..bb86857f8 100644 --- a/state/execution.go +++ b/state/execution.go @@ -372,7 +372,7 @@ func getBeginBlockValidatorInfo(block *types.Block, stateDB dbm.DB) (abci.LastCo // We need the validator set. We already did this in validateBlock. // TODO: Should we instead cache the valset in the evidence itself and add // `SetValidatorSet()` and `ToABCI` methods ? - valset, err := LoadValidators(stateDB, ev.Height()) + valset, err := LoadValidators(stateDB, ev.ValidatorHeight()) if err != nil { panic(err) } diff --git a/state/validation.go b/state/validation.go index ccbcc72e2..e44e43b07 100644 --- a/state/validation.go +++ b/state/validation.go @@ -177,7 +177,7 @@ func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence) error ) } - valset, err := LoadValidators(stateDB, evidence.Height()) + valset, err := LoadValidators(stateDB, evidence.ValidatorHeight()) if err != nil { // TODO: if err is just that we cant find it cuz we pruned, ignore. // TODO: if its actually bad evidence, punish peer @@ -190,7 +190,7 @@ func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence) error // XXX: this makes lite-client bisection as is unsafe // See https://github.com/tendermint/tendermint/issues/3244 ev := evidence - height, addr := ev.Height(), ev.Address() + height, addr := ev.ValidatorHeight(), ev.Address() _, val := valset.GetByAddress(addr) if val == nil { return fmt.Errorf("address %X was not a validator at height %d", addr, height) diff --git a/types/evidence.go b/types/evidence.go index ecea28c69..abc831d7d 100644 --- a/types/evidence.go +++ b/types/evidence.go @@ -58,6 +58,7 @@ func (err *ErrEvidenceOverflow) Error() string { // Evidence represents any provable malicious activity by a validator type Evidence interface { Height() int64 // height of the equivocation + ValidatorHeight() int64 // height of validators Time() time.Time // time of the equivocation Address() []byte // address of the equivocating validator Bytes() []byte // bytes which comprise the evidence @@ -214,6 +215,7 @@ func EvidenceFromProto(evidence *tmproto.Evidence) (Evidence, error) { func RegisterEvidences(cdc *amino.Codec) { cdc.RegisterInterface((*Evidence)(nil), nil) cdc.RegisterConcrete(&DuplicateVoteEvidence{}, "tendermint/DuplicateVoteEvidence", nil) + cdc.RegisterConcrete(&BeaconInactivityEvidence{}, "tendermint/BeaconInactivityEvidence", nil) } func RegisterMockEvidences(cdc *amino.Codec) { @@ -280,6 +282,11 @@ func (dve *DuplicateVoteEvidence) Height() int64 { return dve.VoteA.Height } +// Height returns the height this evidence refers to. +func (dve *DuplicateVoteEvidence) ValidatorHeight() int64 { + return dve.VoteA.Height +} + // Time return the time the evidence was created func (dve *DuplicateVoteEvidence) Time() time.Time { return dve.VoteA.Timestamp @@ -438,9 +445,10 @@ func NewMockEvidence(height int64, eTime time.Time, idx int, address []byte) Moc EvidenceAddress: address} } -func (e MockEvidence) Height() int64 { return e.EvidenceHeight } -func (e MockEvidence) Time() time.Time { return e.EvidenceTime } -func (e MockEvidence) Address() []byte { return e.EvidenceAddress } +func (e MockEvidence) Height() int64 { return e.EvidenceHeight } +func (e MockEvidence) ValidatorHeight() int64 { return e.EvidenceHeight } +func (e MockEvidence) Time() time.Time { return e.EvidenceTime } +func (e MockEvidence) Address() []byte { return e.EvidenceAddress } func (e MockEvidence) Hash() []byte { return []byte(fmt.Sprintf("%d-%x-%s", e.EvidenceHeight, e.EvidenceAddress, e.EvidenceTime)) @@ -471,18 +479,18 @@ func (e MockEvidence) SignBytes(chainID string) []byte { // BeaconInactivityEvidence contains evidence a validator was did not type BeaconInactivityEvidence struct { - CreationHeight int64 // Height evidence was created - CreationTime time.Time // Time evidence was created - DefendantAddress []byte // Address of validator accused of inactivity - ComplainantAddress []byte // Address of validator submitting complaint complaint - AeonStart int64 + CreationHeight int64 // Height evidence was created + CreationTime time.Time // Time evidence was created + DefendantAddress crypto.Address // Address of validator accused of inactivity + ComplainantAddress crypto.Address // Address of validator submitting complaint complaint + AeonStart int64 // Height for fetching validators ComplainantSignature []byte } var _ Evidence = &BeaconInactivityEvidence{} // NewBeaconInactivityEvidence creates BeaconInactivityEvidence -func NewBeaconInactivityEvidence(height int64, defAddress []byte, comAddress []byte, aeon int64) *BeaconInactivityEvidence { +func NewBeaconInactivityEvidence(height int64, defAddress crypto.Address, comAddress crypto.Address, aeon int64) *BeaconInactivityEvidence { return &BeaconInactivityEvidence{ CreationHeight: height, CreationTime: time.Now(), @@ -499,11 +507,16 @@ func (bie *BeaconInactivityEvidence) String() string { } -// Height returns aeon start +// Height returns evidence was created func (bie *BeaconInactivityEvidence) Height() int64 { return bie.CreationHeight } +// Height returns validator height +func (bie *BeaconInactivityEvidence) ValidatorHeight() int64 { + return bie.AeonStart +} + // Time return func (bie *BeaconInactivityEvidence) Time() time.Time { return bie.CreationTime @@ -530,6 +543,9 @@ func (bie *BeaconInactivityEvidence) Verify(chainID string, complainantPubKey cr return fmt.Errorf("ComplainantSignature invalid") } + // Need to verify defendant address in state and also aeon start is correct + // and evidence height is greater than aeon start + return nil } diff --git a/types/params.go b/types/params.go index 11ab66342..fc97bbccc 100644 --- a/types/params.go +++ b/types/params.go @@ -106,8 +106,8 @@ func DefaultValidatorParams() ValidatorParams { func DefaultEntropyParams() EntropyParams { return EntropyParams{ AeonLength: 100, - InactivityWindowSize: 50, // No. of blocks in which we track drb signature shares obtained - RequiredActivityPercentage: 50, // Minimum % of signature shares expected within window + InactivityWindowSize: 100, // No. of blocks in which we track drb signature shares obtained + RequiredActivityPercentage: 50, // Minimum % of signature shares expected within window } } diff --git a/types/protobuf.go b/types/protobuf.go index b9a88717b..1bd02363e 100644 --- a/types/protobuf.go +++ b/types/protobuf.go @@ -16,8 +16,9 @@ import ( // Use strings to distinguish types in ABCI messages const ( - ABCIEvidenceTypeDuplicateVote = "duplicate/vote" - ABCIEvidenceTypeMock = "mock/evidence" + ABCIEvidenceTypeDuplicateVote = "duplicate/vote" + ABCIEvidenceTypeMock = "mock/evidence" + ABCIEvidenceTypeBeaconInactivity = "beacon/inactivity" ) const ( @@ -181,6 +182,8 @@ func (tm2pb) Evidence(ev Evidence, valSet *ValidatorSet, evTime time.Time) abci. case MockEvidence: // XXX: not great to have test types in production paths ... evType = ABCIEvidenceTypeMock + case *BeaconInactivityEvidence: + evType = ABCIEvidenceTypeBeaconInactivity default: panic(fmt.Sprintf("Unknown evidence type: %v %v", ev, reflect.TypeOf(ev))) }