From cf0b5d3715962220fafa0402e8fee7a1b11cda4b Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 22 Dec 2017 14:07:46 -0600 Subject: [PATCH 1/6] enforce <1/3 validator updates Refs #950 --- consensus/reactor_test.go | 4 +- docs/app-development.rst | 18 +-- docs/specification/light-client-protocol.rst | 4 +- state/execution.go | 43 +++++--- state/state_test.go | 109 +++++++++---------- 5 files changed, 91 insertions(+), 87 deletions(-) diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index 7383c790c..e6f5b4b19 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -194,8 +194,8 @@ func TestReactorVotingPowerChange(t *testing.T) { } func TestReactorValidatorSetChanges(t *testing.T) { - nPeers := 7 - nVals := 4 + nPeers := 9 + nVals := 6 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 2614a264c..9574f8630 100644 --- a/docs/app-development.rst +++ b/docs/app-development.rst @@ -403,14 +403,16 @@ pick up from when it restarts. See information on the Handshake, below. EndBlock ^^^^^^^^ -The EndBlock request can be used to run some code at the end of every -block. 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 validator set changes are only available in v0.8.0 -and up. +The EndBlock request can be used to run some code at the end of every block. +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 +`__ +for details on how it tracks validators. .. container:: toggle diff --git a/docs/specification/light-client-protocol.rst b/docs/specification/light-client-protocol.rst index 6c2531d71..6c6083b45 100644 --- a/docs/specification/light-client-protocol.rst +++ b/docs/specification/light-client-protocol.rst @@ -5,8 +5,8 @@ Light clients are an important part of the complete blockchain system for most applications. Tendermint provides unique speed and security properties for light client applications. -See our developing `light-client -repository `__. +See our `lite package +`__. Overview -------- diff --git a/state/execution.go b/state/execution.go index e90c8e710..f2cc3a4ee 100644 --- a/state/execution.go +++ b/state/execution.go @@ -10,7 +10,6 @@ import ( crypto "github.com/tendermint/go-crypto" "github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/types" - cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" ) @@ -112,9 +111,9 @@ func execBlockOnProxyApp(txEventPublisher types.TxEventPublisher, proxyAppConn p return nil, err } - valUpdates := abciResponses.EndBlock.ValidatorUpdates - logger.Info("Executed block", "height", block.Height, "validTxs", validTxs, "invalidTxs", invalidTxs) + + valUpdates := abciResponses.EndBlock.ValidatorUpdates if len(valUpdates) > 0 { logger.Info("Updates to validators", "updates", abci.ValidatorsString(valUpdates)) } @@ -122,10 +121,22 @@ func execBlockOnProxyApp(txEventPublisher types.TxEventPublisher, proxyAppConn p return abciResponses, nil } -func updateValidators(validators *types.ValidatorSet, changedValidators []*abci.Validator) error { - // TODO: prevent change of 1/3+ at once +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 len(updates) > maxUpdates { + return errors.New("Can not update more than 1/3 of validators at once") + } - for _, v := range changedValidators { + for _, v := range updates { pubkey, err := crypto.PubKeyFromBytes(v.PubKey) // NOTE: expects go-wire encoded pubkey if err != nil { return err @@ -135,28 +146,28 @@ func updateValidators(validators *types.ValidatorSet, changedValidators []*abci. power := int64(v.Power) // mind the overflow from int64 if power < 0 { - return errors.New(cmn.Fmt("Power (%d) overflows int64", v.Power)) + return fmt.Errorf("Power (%d) overflows int64", v.Power) } - _, val := validators.GetByAddress(address) + _, val := currentSet.GetByAddress(address) if val == nil { // add val - added := validators.Add(types.NewValidator(pubkey, power)) + added := currentSet.Add(types.NewValidator(pubkey, power)) if !added { - return errors.New(cmn.Fmt("Failed to add new validator %X with voting power %d", address, power)) + return fmt.Errorf("Failed to add new validator %X with voting power %d", address, power) } } else if v.Power == 0 { // remove val - _, removed := validators.Remove(address) + _, removed := currentSet.Remove(address) if !removed { - return errors.New(cmn.Fmt("Failed to remove validator %X)")) + return fmt.Errorf("Failed to remove validator %X", address) } } else { // update val val.VotingPower = power - updated := validators.Update(val) + updated := currentSet.Update(val) if !updated { - return errors.New(cmn.Fmt("Failed to update validator %X with voting power %d", address, power)) + return fmt.Errorf("Failed to update validator %X with voting power %d", address, power) } } } @@ -243,8 +254,8 @@ func (s *State) validateBlock(b *types.Block) error { } } else { if len(b.LastCommit.Precommits) != s.LastValidators.Size() { - return errors.New(cmn.Fmt("Invalid block commit size. Expected %v, got %v", - s.LastValidators.Size(), len(b.LastCommit.Precommits))) + return fmt.Errorf("Invalid block commit size. Expected %v, got %v", + s.LastValidators.Size(), len(b.LastCommit.Precommits)) } err := s.LastValidators.VerifyCommit( s.ChainID, s.LastBlockID, b.Height-1, b.LastCommit) diff --git a/state/state_test.go b/state/state_test.go index 1840d5e15..1df300c7f 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" abci "github.com/tendermint/abci/types" @@ -130,81 +131,56 @@ func TestValidatorSimpleSaveLoad(t *testing.T) { assert.IsType(ErrNoValSetForHeight{}, err, "expected err at unknown height") } -// TestValidatorChangesSaveLoad tests saving and loading a validator set with changes. +// TestValidatorChangesSaveLoad tests saving and loading a validator set with +// changes. func TestValidatorChangesSaveLoad(t *testing.T) { + const valSetSize = 6 tearDown, _, state := setupTestCase(t) + state.Validators = genValSet(valSetSize) + state.Save() defer tearDown(t) - // nolint: vetshadow - assert := assert.New(t) - // change vals at these heights - changeHeights := []int64{1, 2, 4, 5, 10, 15, 16, 17, 20} - N := len(changeHeights) + const height = 1 + pubkey := crypto.GenPrivKeyEd25519().PubKey() + // swap the first validator with a new one ^^^ (validator set size stays the same) + header, parts, responses := makeHeaderPartsResponses(state, height, pubkey) + err := state.SetBlockAndValidators(header, parts, responses) + require.Nil(t, err) + state.saveValidatorsInfo() - // each valset is just one validator. - // create list of them - pubkeys := make([]crypto.PubKey, N+1) - _, val := state.Validators.GetByIndex(0) - pubkeys[0] = val.PubKey - for i := 1; i < N+1; i++ { - pubkeys[i] = crypto.GenPrivKeyEd25519().PubKey() - } + v, err := state.LoadValidators(height + 1) + assert.Nil(t, err) + assert.Equal(t, valSetSize, v.Size()) - // build the validator history by running SetBlockAndValidators - // with the right validator set for each height - highestHeight := changeHeights[N-1] + 5 - changeIndex := 0 - pubkey := pubkeys[changeIndex] - for i := int64(1); i < highestHeight; i++ { - // when we get to a change height, - // use the next pubkey - if changeIndex < len(changeHeights) && i == changeHeights[changeIndex] { - changeIndex++ - pubkey = pubkeys[changeIndex] - } - header, parts, responses := makeHeaderPartsResponses(state, i, pubkey) - state.SetBlockAndValidators(header, parts, responses) - state.saveValidatorsInfo() - } - - // make all the test cases by using the same validator until after the change - testCases := make([]valChangeTestCase, highestHeight) - changeIndex = 0 - pubkey = pubkeys[changeIndex] - for i := int64(1); i < highestHeight+1; i++ { - // we we get to the height after a change height - // use the next pubkey (note our counter starts at 0 this time) - if changeIndex < len(changeHeights) && i == changeHeights[changeIndex]+1 { - changeIndex++ - pubkey = pubkeys[changeIndex] - } - testCases[i-1] = valChangeTestCase{i, pubkey} + index, val := v.GetByAddress(pubkey.Address()) + assert.NotNil(t, val) + if index < 0 { + t.Fatal("expected to find newly added validator") } +} - for _, testCase := range testCases { - v, err := state.LoadValidators(testCase.height) - assert.Nil(err, fmt.Sprintf("expected no err at height %d", testCase.height)) - assert.Equal(v.Size(), 1, "validator set size is greater than 1: %d", v.Size()) - addr, _ := v.GetByIndex(0) - - assert.Equal(addr, testCase.vals.Address(), fmt.Sprintf(`unexpected pubkey at - height %d`, testCase.height)) +func genValSet(size int) *types.ValidatorSet { + vals := make([]*types.Validator, size) + for i := 0; i < size; i++ { + vals[i] = types.NewValidator(crypto.GenPrivKeyEd25519().PubKey(), 10) } + return types.NewValidatorSet(vals) } -// TestConsensusParamsChangesSaveLoad tests saving and loading consensus params with changes. +// TestConsensusParamsChangesSaveLoad tests saving and loading consensus params +// with changes. func TestConsensusParamsChangesSaveLoad(t *testing.T) { tearDown, _, state := setupTestCase(t) + const valSetSize = 20 + state.Validators = genValSet(valSetSize) + state.Save() defer tearDown(t) - // nolint: vetshadow - assert := assert.New(t) // change vals at these heights changeHeights := []int64{1, 2, 4, 5, 10, 15, 16, 17, 20} N := len(changeHeights) - // each valset is just one validator. - // create list of them + // create list of new vals params := make([]types.ConsensusParams, N+1) params[0] = state.ConsensusParams for i := 1; i < N+1; i++ { @@ -225,7 +201,8 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) { cp = params[changeIndex] } header, parts, responses := makeHeaderPartsResponsesParams(state, i, cp) - state.SetBlockAndValidators(header, parts, responses) + err := state.SetBlockAndValidators(header, parts, responses) + require.Nil(t, err) state.saveConsensusParamsInfo() } @@ -245,8 +222,8 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) { for _, testCase := range testCases { p, err := state.LoadConsensusParams(testCase.height) - assert.Nil(err, fmt.Sprintf("expected no err at height %d", testCase.height)) - assert.Equal(testCase.params, p, fmt.Sprintf(`unexpected consensus params at + assert.Nil(t, err, fmt.Sprintf("expected no err at height %d", testCase.height)) + assert.Equal(t, testCase.params, p, fmt.Sprintf(`unexpected consensus params at height %d`, testCase.height)) } } @@ -270,6 +247,20 @@ func makeParams(blockBytes, blockTx, blockGas, txBytes, } } +func TestLessThanOneThirdOfValidatorUpdatesEnforced(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}}}, + } + err := state.SetBlockAndValidators(block.Header, types.PartSetHeader{}, abciResponses) + assert.NotNil(t, err, "expected err when trying to update more than 1/3 of validators") +} + func TestApplyUpdates(t *testing.T) { initParams := makeParams(1, 2, 3, 4, 5, 6) From 0093f9877ae476a379fb2a338639f98afbe547c4 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 25 Dec 2017 14:07:45 -0600 Subject: [PATCH 2/6] change voting power change, not number of vals --- consensus/common_test.go | 2 +- consensus/reactor_test.go | 6 ++--- docs/app-development.rst | 9 ++++--- state/execution.go | 55 +++++++++++++++++++++++++++++++-------- state/state_test.go | 19 +++++++------- 5 files changed, 63 insertions(+), 28 deletions(-) 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) { From 9b25f7325a06bff8ad32022c097a4891c6f94656 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 25 Dec 2017 17:53:54 -0600 Subject: [PATCH 3/6] update docs [ci skip] --- docs/app-development.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/app-development.rst b/docs/app-development.rst index e373bcff1..fd6d3f7f3 100644 --- a/docs/app-development.rst +++ b/docs/app-development.rst @@ -409,11 +409,11 @@ 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 the change in voting power -must be strictly less than 1/3 because otherwise it will be impossible for a +must be strictly less than 1/3 per block. 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. Tendermint core will report an error -if that is the case. +for details on how it tracks validators. Tendermint core will fail with an +error if the change in voting power is more or equal than 1/3. .. container:: toggle From 6b89639f90f9542de39c12a513d6490b966f9ba4 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 25 Dec 2017 17:58:15 -0600 Subject: [PATCH 4/6] update docs 2 [ci skip] --- docs/app-development.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/app-development.rst b/docs/app-development.rst index fd6d3f7f3..cbb39fa34 100644 --- a/docs/app-development.rst +++ b/docs/app-development.rst @@ -424,8 +424,8 @@ error if the change in voting power is more or equal than 1/3. .. code-block:: go // Update the validator set - func (app *PersistentDummyApplication) EndBlock(height uint64) (resEndBlock types.ResponseEndBlock) { - return types.ResponseEndBlock{Diffs: app.changes} + func (app *PersistentDummyApplication) EndBlock(req types.RequestEndBlock) types.ResponseEndBlock { + return types.ResponseEndBlock{ValidatorUpdates: app.ValUpdates} } .. container:: toggle From b8215d8ac8595aa28aa155cc3650e744e3debdfe Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 26 Dec 2017 12:51:51 -0600 Subject: [PATCH 5/6] more test cases --- state/state_test.go | 77 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/state/state_test.go b/state/state_test.go index 36e8f6975..0a79236ec 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -246,20 +246,75 @@ func makeParams(blockBytes, blockTx, blockGas, txBytes, } func TestLessThanOneThirdOfVotingPowerPerBlockEnforced(t *testing.T) { - tearDown, _, state := setupTestCase(t) - defer tearDown(t) + testCases := []struct { + initialValSetSize int + shouldErr bool + valUpdatesFn func(vals *types.ValidatorSet) []*abci.Validator + }{ + ///////////// 1 val (vp: 10) => less than 3 is ok //////////////////////// + // adding 1 validator => 10 + 0: {1, false, func(vals *types.ValidatorSet) []*abci.Validator { + return []*abci.Validator{ + {PubKey: pk(), Power: 2}, + } + }}, + 1: {1, true, func(vals *types.ValidatorSet) []*abci.Validator { + return []*abci.Validator{ + {PubKey: pk(), Power: 3}, + } + }}, + 2: {1, true, func(vals *types.ValidatorSet) []*abci.Validator { + return []*abci.Validator{ + {PubKey: pk(), Power: 100}, + } + }}, - height := state.LastBlockHeight + 1 - block := makeBlock(state, height) - abciResponses := &ABCIResponses{ - Height: height, - // 1 val (vp: 10) => less than 3 is ok - EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []*abci.Validator{ - {PubKey: crypto.GenPrivKeyEd25519().PubKey().Bytes(), Power: 3}, + ///////////// 3 val (vp: 30) => less than 10 is ok //////////////////////// + // adding and removing validator => 20 + 3: {3, true, func(vals *types.ValidatorSet) []*abci.Validator { + _, firstVal := vals.GetByIndex(0) + return []*abci.Validator{ + {PubKey: firstVal.PubKey.Bytes(), Power: 0}, + {PubKey: pk(), Power: 10}, + } }}, + // adding 1 validator => 10 + 4: {3, true, func(vals *types.ValidatorSet) []*abci.Validator { + return []*abci.Validator{ + {PubKey: pk(), Power: 10}, + } + }}, + // adding 2 validators => 8 + 5: {3, false, func(vals *types.ValidatorSet) []*abci.Validator { + return []*abci.Validator{ + {PubKey: pk(), Power: 4}, + {PubKey: pk(), Power: 4}, + } + }}, + } + + for i, tc := range testCases { + tearDown, _, state := setupTestCase(t) + state.Validators = genValSet(tc.initialValSetSize) + state.Save() + height := state.LastBlockHeight + 1 + block := makeBlock(state, height) + abciResponses := &ABCIResponses{ + Height: height, + EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: tc.valUpdatesFn(state.Validators)}, + } + err := state.SetBlockAndValidators(block.Header, types.PartSetHeader{}, abciResponses) + if tc.shouldErr { + assert.Error(t, err, "#%d", i) + } else { + assert.NoError(t, err, "#%d", i) + } + tearDown(t) } - err := state.SetBlockAndValidators(block.Header, types.PartSetHeader{}, abciResponses) - assert.Error(t, err) +} + +func pk() []byte { + return crypto.GenPrivKeyEd25519().PubKey().Bytes() } func TestApplyUpdates(t *testing.T) { From 3ea1145486926deb7adffe1f00718e0b902fca61 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 26 Dec 2017 19:22:15 -0500 Subject: [PATCH 6/6] bring back test --- state/state_test.go | 84 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/state/state_test.go b/state/state_test.go index 0a79236ec..b46996155 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -131,9 +131,62 @@ func TestValidatorSimpleSaveLoad(t *testing.T) { assert.IsType(ErrNoValSetForHeight{}, err, "expected err at unknown height") } +// TestValidatorChangesSaveLoad tests saving and loading a validator set with changes. +func TestOneValidatorChangesSaveLoad(t *testing.T) { + tearDown, _, state := setupTestCase(t) + defer tearDown(t) + + // change vals at these heights + changeHeights := []int64{1, 2, 4, 5, 10, 15, 16, 17, 20} + N := len(changeHeights) + + // build the validator history by running SetBlockAndValidators + // with the right validator set for each height + highestHeight := changeHeights[N-1] + 5 + changeIndex := 0 + _, val := state.Validators.GetByIndex(0) + power := val.VotingPower + for i := int64(1); i < highestHeight; i++ { + // when we get to a change height, + // use the next pubkey + if changeIndex < len(changeHeights) && i == changeHeights[changeIndex] { + changeIndex++ + power += 1 + } + header, parts, responses := makeHeaderPartsResponsesValPowerChange(state, i, power) + err := state.SetBlockAndValidators(header, parts, responses) + assert.Nil(t, err) + state.saveValidatorsInfo() + } + + // on each change height, increment the power by one. + testCases := make([]int64, highestHeight) + changeIndex = 0 + power = val.VotingPower + for i := int64(1); i < highestHeight+1; i++ { + // we we get to the height after a change height + // use the next pubkey (note our counter starts at 0 this time) + if changeIndex < len(changeHeights) && i == changeHeights[changeIndex]+1 { + changeIndex++ + power += 1 + } + testCases[i-1] = power + } + + for i, power := range testCases { + v, err := state.LoadValidators(int64(i + 1)) + assert.Nil(t, err, fmt.Sprintf("expected no err at height %d", i)) + assert.Equal(t, v.Size(), 1, "validator set size is greater than 1: %d", v.Size()) + _, val := v.GetByIndex(0) + + assert.Equal(t, val.VotingPower, power, fmt.Sprintf(`unexpected powerat + height %d`, i)) + } +} + // TestValidatorChangesSaveLoad tests saving and loading a validator set with // changes. -func TestValidatorChangesSaveLoad(t *testing.T) { +func TestManyValidatorChangesSaveLoad(t *testing.T) { const valSetSize = 7 tearDown, _, state := setupTestCase(t) state.Validators = genValSet(valSetSize) @@ -143,7 +196,7 @@ func TestValidatorChangesSaveLoad(t *testing.T) { const height = 1 pubkey := crypto.GenPrivKeyEd25519().PubKey() // swap the first validator with a new one ^^^ (validator set size stays the same) - header, parts, responses := makeHeaderPartsResponses(state, height, pubkey) + header, parts, responses := makeHeaderPartsResponsesValPubKeyChange(state, height, pubkey) err := state.SetBlockAndValidators(header, parts, responses) require.Nil(t, err) state.saveValidatorsInfo() @@ -363,17 +416,17 @@ func TestApplyUpdates(t *testing.T) { } } -func makeHeaderPartsResponses(state *State, height int64, +func makeHeaderPartsResponsesValPubKeyChange(state *State, height int64, pubkey crypto.PubKey) (*types.Header, types.PartSetHeader, *ABCIResponses) { block := makeBlock(state, height) - _, val := state.Validators.GetByIndex(0) abciResponses := &ABCIResponses{ Height: height, EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []*abci.Validator{}}, } // if the pubkey is new, remove the old and add the new + _, val := state.Validators.GetByIndex(0) if !bytes.Equal(pubkey.Bytes(), val.PubKey.Bytes()) { abciResponses.EndBlock = &abci.ResponseEndBlock{ ValidatorUpdates: []*abci.Validator{ @@ -386,9 +439,26 @@ func makeHeaderPartsResponses(state *State, height int64, return block.Header, types.PartSetHeader{}, abciResponses } -type valChangeTestCase struct { - height int64 - vals crypto.PubKey +func makeHeaderPartsResponsesValPowerChange(state *State, height int64, + power int64) (*types.Header, types.PartSetHeader, *ABCIResponses) { + + block := makeBlock(state, height) + abciResponses := &ABCIResponses{ + Height: height, + EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []*abci.Validator{}}, + } + + // if the pubkey is new, remove the old and add the new + _, val := state.Validators.GetByIndex(0) + if val.VotingPower != power { + abciResponses.EndBlock = &abci.ResponseEndBlock{ + ValidatorUpdates: []*abci.Validator{ + {val.PubKey.Bytes(), power}, + }, + } + } + + return block.Header, types.PartSetHeader{}, abciResponses } func makeHeaderPartsResponsesParams(state *State, height int64,