Browse Source

Merge branch '950-enforce-less-13-val-changes-per-block' into develop

pull/1016/head
Ethan Buchman 7 years ago
parent
commit
d844799b3b
6 changed files with 234 additions and 70 deletions
  1. +1
    -1
      consensus/common_test.go
  2. +1
    -1
      consensus/reactor_test.go
  3. +13
    -10
      docs/app-development.rst
  4. +2
    -2
      docs/specification/light-client-protocol.rst
  5. +60
    -16
      state/execution.go
  6. +157
    -40
      state/state_test.go

+ 1
- 1
consensus/common_test.go View File

@ -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++ {


+ 1
- 1
consensus/reactor_test.go View File

@ -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)


+ 13
- 10
docs/app-development.rst View File

@ -403,14 +403,17 @@ 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 the change in voting power
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
<https://godoc.org/github.com/tendermint/tendermint/lite#hdr-How_We_Track_Validators>`__
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
@ -421,8 +424,8 @@ and up.
.. 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


+ 2
- 2
docs/specification/light-client-protocol.rst 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
properties for light client applications.
See our developing `light-client
repository <https://github.com/tendermint/light-client>`__.
See our `lite package
<https://godoc.org/github.com/tendermint/tendermint/lite>`__.
Overview
--------


+ 60
- 16
state/execution.go View File

@ -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,19 @@ 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 {
// 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 vp23 {
return errors.New("the change in voting power must be strictly less than 1/3")
}
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,34 +143,70 @@ 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)
}
}
}
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
@ -243,8 +287,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)


+ 157
- 40
state/state_test.go View File

@ -6,6 +6,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/abci/types"
@ -131,79 +132,105 @@ func TestValidatorSimpleSaveLoad(t *testing.T) {
}
// TestValidatorChangesSaveLoad tests saving and loading a validator set with changes.
func TestValidatorChangesSaveLoad(t *testing.T) {
func TestOneValidatorChangesSaveLoad(t *testing.T) {
tearDown, _, state := setupTestCase(t)
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
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]
_, 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++
pubkey = pubkeys[changeIndex]
power += 1
}
header, parts, responses := makeHeaderPartsResponses(state, i, pubkey)
state.SetBlockAndValidators(header, parts, responses)
header, parts, responses := makeHeaderPartsResponsesValPowerChange(state, i, power)
err := state.SetBlockAndValidators(header, parts, responses)
assert.Nil(t, err)
state.saveValidatorsInfo()
}
// make all the test cases by using the same validator until after the change
testCases := make([]valChangeTestCase, highestHeight)
// on each change height, increment the power by one.
testCases := make([]int64, highestHeight)
changeIndex = 0
pubkey = pubkeys[changeIndex]
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++
pubkey = pubkeys[changeIndex]
power += 1
}
testCases[i-1] = valChangeTestCase{i, pubkey}
testCases[i-1] = power
}
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)
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(addr, testCase.vals.Address(), fmt.Sprintf(`unexpected pubkey at
height %d`, testCase.height))
assert.Equal(t, val.VotingPower, power, fmt.Sprintf(`unexpected powerat
height %d`, i))
}
}
// TestConsensusParamsChangesSaveLoad tests saving and loading consensus params with changes.
// TestValidatorChangesSaveLoad tests saving and loading a validator set with
// changes.
func TestManyValidatorChangesSaveLoad(t *testing.T) {
const valSetSize = 7
tearDown, _, state := setupTestCase(t)
state.Validators = genValSet(valSetSize)
state.Save()
defer tearDown(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 := makeHeaderPartsResponsesValPubKeyChange(state, height, pubkey)
err := state.SetBlockAndValidators(header, parts, responses)
require.Nil(t, err)
state.saveValidatorsInfo()
v, err := state.LoadValidators(height + 1)
assert.Nil(t, err)
assert.Equal(t, valSetSize, v.Size())
index, val := v.GetByAddress(pubkey.Address())
assert.NotNil(t, val)
if index < 0 {
t.Fatal("expected to find newly added validator")
}
}
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) {
tearDown, _, state := setupTestCase(t)
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.
// each valset is just one validator
// create list of them
params := make([]types.ConsensusParams, N+1)
params[0] = state.ConsensusParams
@ -225,7 +252,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 +273,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 +298,78 @@ func makeParams(blockBytes, blockTx, blockGas, txBytes,
}
}
func TestLessThanOneThirdOfVotingPowerPerBlockEnforced(t *testing.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},
}
}},
///////////// 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)
}
}
func pk() []byte {
return crypto.GenPrivKeyEd25519().PubKey().Bytes()
}
func TestApplyUpdates(t *testing.T) {
initParams := makeParams(1, 2, 3, 4, 5, 6)
@ -316,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{
@ -339,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,


Loading…
Cancel
Save