enforce <1/3 validator updates

Refs #950
This commit is contained in:
Anton Kaliaev 2017-12-22 14:07:46 -06:00
parent 616f7e74db
commit cf0b5d3715
No known key found for this signature in database
GPG Key ID: 7B6881D965918214
5 changed files with 93 additions and 89 deletions

View File

@ -194,8 +194,8 @@ func TestReactorVotingPowerChange(t *testing.T) {
} }
func TestReactorValidatorSetChanges(t *testing.T) { func TestReactorValidatorSetChanges(t *testing.T) {
nPeers := 7 nPeers := 9
nVals := 4 nVals := 6
css := randConsensusNetWithPeers(nVals, nPeers, "consensus_val_set_changes_test", newMockTickerFunc(true), newPersistentDummy) css := randConsensusNetWithPeers(nVals, nPeers, "consensus_val_set_changes_test", newMockTickerFunc(true), newPersistentDummy)
logger := log.TestingLogger() logger := log.TestingLogger()

View File

@ -403,14 +403,16 @@ pick up from when it restarts. See information on the Handshake, below.
EndBlock EndBlock
^^^^^^^^ ^^^^^^^^
The EndBlock request can be used to run some code at the end of every The EndBlock request can be used to run some code at the end of every block.
block. Additionally, the response may contain a list of validators, Additionally, the response may contain a list of validators, which can be used
which can be used to update the validator set. To add a new validator or to update the validator set. To add a new validator or update an existing one,
update an existing one, simply include them in the list returned in the simply include them in the list returned in the EndBlock response. To remove
EndBlock response. To remove one, include it in the list with a one, include it in the list with a ``power`` equal to ``0``. Tendermint core
``power`` equal to ``0``. Tendermint core will take care of updating the will take care of updating the validator set. Note you can not update more than
validator set. Note validator set changes are only available in v0.8.0 1/3 of validators in one block because this will make it impossible for a light
and up. client to prove the transition externally. See the `light client docs
<https://godoc.org/github.com/tendermint/tendermint/lite#hdr-How_We_Track_Validators>`__
for details on how it tracks validators.
.. container:: toggle .. container:: toggle

View File

@ -5,8 +5,8 @@ Light clients are an important part of the complete blockchain system
for most applications. Tendermint provides unique speed and security for most applications. Tendermint provides unique speed and security
properties for light client applications. properties for light client applications.
See our developing `light-client See our `lite package
repository <https://github.com/tendermint/light-client>`__. <https://godoc.org/github.com/tendermint/tendermint/lite>`__.
Overview Overview
-------- --------

View File

@ -10,7 +10,6 @@ import (
crypto "github.com/tendermint/go-crypto" crypto "github.com/tendermint/go-crypto"
"github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/proxy"
"github.com/tendermint/tendermint/types" "github.com/tendermint/tendermint/types"
cmn "github.com/tendermint/tmlibs/common"
"github.com/tendermint/tmlibs/log" "github.com/tendermint/tmlibs/log"
) )
@ -112,9 +111,9 @@ func execBlockOnProxyApp(txEventPublisher types.TxEventPublisher, proxyAppConn p
return nil, err return nil, err
} }
valUpdates := abciResponses.EndBlock.ValidatorUpdates
logger.Info("Executed block", "height", block.Height, "validTxs", validTxs, "invalidTxs", invalidTxs) logger.Info("Executed block", "height", block.Height, "validTxs", validTxs, "invalidTxs", invalidTxs)
valUpdates := abciResponses.EndBlock.ValidatorUpdates
if len(valUpdates) > 0 { if len(valUpdates) > 0 {
logger.Info("Updates to validators", "updates", abci.ValidatorsString(valUpdates)) logger.Info("Updates to validators", "updates", abci.ValidatorsString(valUpdates))
} }
@ -122,10 +121,22 @@ func execBlockOnProxyApp(txEventPublisher types.TxEventPublisher, proxyAppConn p
return abciResponses, nil return abciResponses, nil
} }
func updateValidators(validators *types.ValidatorSet, changedValidators []*abci.Validator) error { func updateValidators(currentSet *types.ValidatorSet, updates []*abci.Validator) error {
// TODO: prevent change of 1/3+ at once // ## 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 pubkey, err := crypto.PubKeyFromBytes(v.PubKey) // NOTE: expects go-wire encoded pubkey
if err != nil { if err != nil {
return err return err
@ -135,28 +146,28 @@ func updateValidators(validators *types.ValidatorSet, changedValidators []*abci.
power := int64(v.Power) power := int64(v.Power)
// mind the overflow from int64 // mind the overflow from int64
if power < 0 { 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 { if val == nil {
// add val // add val
added := validators.Add(types.NewValidator(pubkey, power)) added := currentSet.Add(types.NewValidator(pubkey, power))
if !added { 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 { } else if v.Power == 0 {
// remove val // remove val
_, removed := validators.Remove(address) _, removed := currentSet.Remove(address)
if !removed { if !removed {
return errors.New(cmn.Fmt("Failed to remove validator %X)")) return fmt.Errorf("Failed to remove validator %X", address)
} }
} else { } else {
// update val // update val
val.VotingPower = power val.VotingPower = power
updated := validators.Update(val) updated := currentSet.Update(val)
if !updated { 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 { } else {
if len(b.LastCommit.Precommits) != s.LastValidators.Size() { if len(b.LastCommit.Precommits) != s.LastValidators.Size() {
return errors.New(cmn.Fmt("Invalid block commit size. Expected %v, got %v", return fmt.Errorf("Invalid block commit size. Expected %v, got %v",
s.LastValidators.Size(), len(b.LastCommit.Precommits))) s.LastValidators.Size(), len(b.LastCommit.Precommits))
} }
err := s.LastValidators.VerifyCommit( err := s.LastValidators.VerifyCommit(
s.ChainID, s.LastBlockID, b.Height-1, b.LastCommit) s.ChainID, s.LastBlockID, b.Height-1, b.LastCommit)

View File

@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/abci/types" abci "github.com/tendermint/abci/types"
@ -130,81 +131,56 @@ func TestValidatorSimpleSaveLoad(t *testing.T) {
assert.IsType(ErrNoValSetForHeight{}, err, "expected err at unknown height") 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) { func TestValidatorChangesSaveLoad(t *testing.T) {
const valSetSize = 6
tearDown, _, state := setupTestCase(t) tearDown, _, state := setupTestCase(t)
state.Validators = genValSet(valSetSize)
state.Save()
defer tearDown(t) defer tearDown(t)
// nolint: vetshadow
assert := assert.New(t)
// change vals at these heights const height = 1
changeHeights := []int64{1, 2, 4, 5, 10, 15, 16, 17, 20} pubkey := crypto.GenPrivKeyEd25519().PubKey()
N := len(changeHeights) // swap the first validator with a new one ^^^ (validator set size stays the same)
header, parts, responses := makeHeaderPartsResponses(state, height, pubkey)
// each valset is just one validator. err := state.SetBlockAndValidators(header, parts, responses)
// create list of them require.Nil(t, err)
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()
}
// 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() state.saveValidatorsInfo()
}
// make all the test cases by using the same validator until after the change v, err := state.LoadValidators(height + 1)
testCases := make([]valChangeTestCase, highestHeight) assert.Nil(t, err)
changeIndex = 0 assert.Equal(t, valSetSize, v.Size())
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}
}
for _, testCase := range testCases { index, val := v.GetByAddress(pubkey.Address())
v, err := state.LoadValidators(testCase.height) assert.NotNil(t, val)
assert.Nil(err, fmt.Sprintf("expected no err at height %d", testCase.height)) if index < 0 {
assert.Equal(v.Size(), 1, "validator set size is greater than 1: %d", v.Size()) t.Fatal("expected to find newly added validator")
addr, _ := v.GetByIndex(0)
assert.Equal(addr, testCase.vals.Address(), fmt.Sprintf(`unexpected pubkey at
height %d`, testCase.height))
} }
} }
// TestConsensusParamsChangesSaveLoad tests saving and loading consensus params with changes. 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.
func TestConsensusParamsChangesSaveLoad(t *testing.T) { func TestConsensusParamsChangesSaveLoad(t *testing.T) {
tearDown, _, state := setupTestCase(t) tearDown, _, state := setupTestCase(t)
const valSetSize = 20
state.Validators = genValSet(valSetSize)
state.Save()
defer tearDown(t) defer tearDown(t)
// nolint: vetshadow
assert := assert.New(t)
// change vals at these heights // change vals at these heights
changeHeights := []int64{1, 2, 4, 5, 10, 15, 16, 17, 20} changeHeights := []int64{1, 2, 4, 5, 10, 15, 16, 17, 20}
N := len(changeHeights) N := len(changeHeights)
// each valset is just one validator. // create list of new vals
// create list of them
params := make([]types.ConsensusParams, N+1) params := make([]types.ConsensusParams, N+1)
params[0] = state.ConsensusParams params[0] = state.ConsensusParams
for i := 1; i < N+1; i++ { for i := 1; i < N+1; i++ {
@ -225,7 +201,8 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) {
cp = params[changeIndex] cp = params[changeIndex]
} }
header, parts, responses := makeHeaderPartsResponsesParams(state, i, cp) header, parts, responses := makeHeaderPartsResponsesParams(state, i, cp)
state.SetBlockAndValidators(header, parts, responses) err := state.SetBlockAndValidators(header, parts, responses)
require.Nil(t, err)
state.saveConsensusParamsInfo() state.saveConsensusParamsInfo()
} }
@ -245,8 +222,8 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) {
for _, testCase := range testCases { for _, testCase := range testCases {
p, err := state.LoadConsensusParams(testCase.height) p, err := state.LoadConsensusParams(testCase.height)
assert.Nil(err, fmt.Sprintf("expected no err at height %d", testCase.height)) assert.Nil(t, err, fmt.Sprintf("expected no err at height %d", testCase.height))
assert.Equal(testCase.params, p, fmt.Sprintf(`unexpected consensus params at assert.Equal(t, testCase.params, p, fmt.Sprintf(`unexpected consensus params at
height %d`, testCase.height)) 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) { func TestApplyUpdates(t *testing.T) {
initParams := makeParams(1, 2, 3, 4, 5, 6) initParams := makeParams(1, 2, 3, 4, 5, 6)