diff --git a/consensus/common_test.go b/consensus/common_test.go index 377f1929f..dd6fce660 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -347,7 +347,7 @@ func consensusLogger() log.Logger { } func randConsensusNet(nValidators int, testName string, tickerFunc func() TimeoutTicker, appFunc func() abci.Application, configOpts ...func(*cfg.Config)) []*ConsensusState { - genDoc, privVals := randGenesisDoc(nValidators, false, 10) + genDoc, privVals := randGenesisDoc(nValidators, false, 30) css := make([]*ConsensusState, nValidators) logger := consensusLogger() for i := 0; i < nValidators; i++ { diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index e6f5b4b19..bcf97f94b 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -180,7 +180,7 @@ func TestReactorVotingPowerChange(t *testing.T) { t.Fatalf("expected voting power to change (before: %d, after: %d)", previousTotalVotingPower, css[0].GetRoundState().LastValidators.TotalVotingPower()) } - updateValidatorTx = dummy.MakeValSetChangeTx(val1PubKey.Bytes(), 100) + updateValidatorTx = dummy.MakeValSetChangeTx(val1PubKey.Bytes(), 26) previousTotalVotingPower = css[0].GetRoundState().LastValidators.TotalVotingPower() waitForAndValidateBlock(t, nVals, activeVals, eventChans, css, updateValidatorTx) @@ -194,8 +194,8 @@ func TestReactorVotingPowerChange(t *testing.T) { } func TestReactorValidatorSetChanges(t *testing.T) { - nPeers := 9 - nVals := 6 + nPeers := 7 + nVals := 4 css := randConsensusNetWithPeers(nVals, nPeers, "consensus_val_set_changes_test", newMockTickerFunc(true), newPersistentDummy) logger := log.TestingLogger() diff --git a/docs/app-development.rst b/docs/app-development.rst index 9574f8630..e373bcff1 100644 --- a/docs/app-development.rst +++ b/docs/app-development.rst @@ -408,11 +408,12 @@ Additionally, the response may contain a list of validators, which can be used to update the validator set. To add a new validator or update an existing one, simply include them in the list returned in the EndBlock response. To remove one, include it in the list with a ``power`` equal to ``0``. Tendermint core -will take care of updating the validator set. Note you can not update more than -1/3 of validators in one block because this will make it impossible for a light -client to prove the transition externally. See the `light client docs +will take care of updating the validator set. Note the change in voting power +must be strictly less than 1/3 because otherwise it will be impossible for a +light client to prove the transition externally. See the `light client docs `__ -for details on how it tracks validators. +for details on how it tracks validators. Tendermint core will report an error +if that is the case. .. container:: toggle diff --git a/state/execution.go b/state/execution.go index f2cc3a4ee..d626ee0fc 100644 --- a/state/execution.go +++ b/state/execution.go @@ -122,18 +122,15 @@ func execBlockOnProxyApp(txEventPublisher types.TxEventPublisher, proxyAppConn p } func updateValidators(currentSet *types.ValidatorSet, updates []*abci.Validator) error { - // ## prevent update of 1/3+ at once - // - // If more than 1/3 validators changed in one block, then a light - // client could never prove the transition externally. See - // ./lite/doc.go for details on how a light client tracks - // validators. - maxUpdates := currentSet.Size() / 3 - if maxUpdates == 0 { // if current set size is less than 3 - maxUpdates = 1 + // If more or equal than 1/3 of total voting power changed in one block, then + // a light client could never prove the transition externally. See + // ./lite/doc.go for details on how a light client tracks validators. + vp23, err := changeInVotingPowerMoreOrEqualToOneThird(currentSet, updates) + if err != nil { + return err } - if len(updates) > maxUpdates { - return errors.New("Can not update more than 1/3 of validators at once") + if vp23 { + return errors.New("the change in voting power must be strictly less than 1/3") } for _, v := range updates { @@ -174,6 +171,42 @@ func updateValidators(currentSet *types.ValidatorSet, updates []*abci.Validator) return nil } +func changeInVotingPowerMoreOrEqualToOneThird(currentSet *types.ValidatorSet, updates []*abci.Validator) (bool, error) { + threshold := currentSet.TotalVotingPower() * 1 / 3 + acc := int64(0) + + for _, v := range updates { + pubkey, err := crypto.PubKeyFromBytes(v.PubKey) // NOTE: expects go-wire encoded pubkey + if err != nil { + return false, err + } + + address := pubkey.Address() + power := int64(v.Power) + // mind the overflow from int64 + if power < 0 { + return false, fmt.Errorf("Power (%d) overflows int64", v.Power) + } + + _, val := currentSet.GetByAddress(address) + if val == nil { + acc += power + } else { + np := val.VotingPower - power + if np < 0 { + np = -np + } + acc += np + } + + if acc >= threshold { + return true, nil + } + } + + return false, nil +} + // return a bit array of validators that signed the last commit // NOTE: assumes commits have already been authenticated /* function is currently unused diff --git a/state/state_test.go b/state/state_test.go index 1df300c7f..36e8f6975 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -134,7 +134,7 @@ func TestValidatorSimpleSaveLoad(t *testing.T) { // TestValidatorChangesSaveLoad tests saving and loading a validator set with // changes. func TestValidatorChangesSaveLoad(t *testing.T) { - const valSetSize = 6 + const valSetSize = 7 tearDown, _, state := setupTestCase(t) state.Validators = genValSet(valSetSize) state.Save() @@ -171,16 +171,14 @@ func genValSet(size int) *types.ValidatorSet { // with changes. func TestConsensusParamsChangesSaveLoad(t *testing.T) { tearDown, _, state := setupTestCase(t) - const valSetSize = 20 - state.Validators = genValSet(valSetSize) - state.Save() defer tearDown(t) // change vals at these heights changeHeights := []int64{1, 2, 4, 5, 10, 15, 16, 17, 20} N := len(changeHeights) - // create list of new vals + // each valset is just one validator + // create list of them params := make([]types.ConsensusParams, N+1) params[0] = state.ConsensusParams for i := 1; i < N+1; i++ { @@ -247,18 +245,21 @@ func makeParams(blockBytes, blockTx, blockGas, txBytes, } } -func TestLessThanOneThirdOfValidatorUpdatesEnforced(t *testing.T) { +func TestLessThanOneThirdOfVotingPowerPerBlockEnforced(t *testing.T) { tearDown, _, state := setupTestCase(t) defer tearDown(t) height := state.LastBlockHeight + 1 block := makeBlock(state, height) abciResponses := &ABCIResponses{ - Height: height, - EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []*abci.Validator{{PubKey: []byte("a"), Power: 10}}}, + Height: height, + // 1 val (vp: 10) => less than 3 is ok + EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []*abci.Validator{ + {PubKey: crypto.GenPrivKeyEd25519().PubKey().Bytes(), Power: 3}, + }}, } err := state.SetBlockAndValidators(block.Header, types.PartSetHeader{}, abciResponses) - assert.NotNil(t, err, "expected err when trying to update more than 1/3 of validators") + assert.Error(t, err) } func TestApplyUpdates(t *testing.T) {