cmd: add integration test for rollback functionality (backport #7315) (#7368)

* cmd: add integration test and fix bug in rollback command (#7315)

(cherry picked from commit bca2080c01)

Co-authored-by: Callum Waters <cmwaters19@gmail.com>
This commit is contained in:
mergify[bot]
2021-12-02 08:30:57 -08:00
committed by GitHub
co-authored by Callum Waters
parent 9994396e59
commit 05340ca069
10 changed files with 202 additions and 118 deletions
+14
View File
@@ -33,6 +33,20 @@ func (_m *Store) Bootstrap(_a0 state.State) error {
return r0
}
// Close provides a mock function with given fields:
func (_m *Store) Close() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Load provides a mock function with given fields:
func (_m *Store) Load() (state.State, error) {
ret := _m.Called()
+25 -8
View File
@@ -21,18 +21,35 @@ func Rollback(bs BlockStore, ss Store) (int64, []byte, error) {
return -1, nil, errors.New("no state found")
}
rollbackHeight := invalidState.LastBlockHeight
height := bs.Height()
// NOTE: persistence of state and blocks don't happen atomically. Therefore it is possible that
// when the user stopped the node the state wasn't updated but the blockstore was. In this situation
// we don't need to rollback any state and can just return early
if height == invalidState.LastBlockHeight+1 {
return invalidState.LastBlockHeight, invalidState.AppHash, nil
}
// If the state store isn't one below nor equal to the blockstore height than this violates the
// invariant
if height != invalidState.LastBlockHeight {
return -1, nil, fmt.Errorf("statestore height (%d) is not one below or equal to blockstore height (%d)",
invalidState.LastBlockHeight, height)
}
// state store height is equal to blockstore height. We're good to proceed with rolling back state
rollbackHeight := invalidState.LastBlockHeight - 1
rollbackBlock := bs.LoadBlockMeta(rollbackHeight)
if rollbackBlock == nil {
return -1, nil, fmt.Errorf("block at height %d not found", rollbackHeight)
}
previousValidatorSet, err := ss.LoadValidators(rollbackHeight - 1)
previousLastValidatorSet, err := ss.LoadValidators(rollbackHeight)
if err != nil {
return -1, nil, err
}
previousParams, err := ss.LoadConsensusParams(rollbackHeight)
previousParams, err := ss.LoadConsensusParams(rollbackHeight + 1)
if err != nil {
return -1, nil, err
}
@@ -40,13 +57,13 @@ func Rollback(bs BlockStore, ss Store) (int64, []byte, error) {
valChangeHeight := invalidState.LastHeightValidatorsChanged
// this can only happen if the validator set changed since the last block
if valChangeHeight > rollbackHeight {
valChangeHeight = rollbackHeight
valChangeHeight = rollbackHeight + 1
}
paramsChangeHeight := invalidState.LastHeightConsensusParamsChanged
// this can only happen if params changed from the last block
if paramsChangeHeight > rollbackHeight {
paramsChangeHeight = rollbackHeight
paramsChangeHeight = rollbackHeight + 1
}
// build the new state from the old state and the prior block
@@ -62,13 +79,13 @@ func Rollback(bs BlockStore, ss Store) (int64, []byte, error) {
ChainID: invalidState.ChainID,
InitialHeight: invalidState.InitialHeight,
LastBlockHeight: invalidState.LastBlockHeight - 1,
LastBlockID: rollbackBlock.Header.LastBlockID,
LastBlockHeight: rollbackBlock.Header.Height,
LastBlockID: rollbackBlock.BlockID,
LastBlockTime: rollbackBlock.Header.Time,
NextValidators: invalidState.Validators,
Validators: invalidState.LastValidators,
LastValidators: previousValidatorSet,
LastValidators: previousLastValidatorSet,
LastHeightValidatorsChanged: valChangeHeight,
ConsensusParams: previousParams,
+86 -100
View File
@@ -17,19 +17,97 @@ import (
)
func TestRollback(t *testing.T) {
var (
height int64 = 100
nextHeight int64 = 101
)
blockStore := &mocks.BlockStore{}
stateStore := setupStateStore(t, height)
initialState, err := stateStore.Load()
require.NoError(t, err)
// perform the rollback over a version bump
newParams := types.DefaultConsensusParams()
newParams.Version.AppVersion = 11
newParams.Block.MaxBytes = 1000
nextState := initialState.Copy()
nextState.LastBlockHeight = nextHeight
nextState.Version.Consensus.App = 11
nextState.LastBlockID = makeBlockIDRandom()
nextState.AppHash = tmhash.Sum([]byte("app_hash"))
nextState.LastValidators = initialState.Validators
nextState.Validators = initialState.NextValidators
nextState.NextValidators = initialState.NextValidators.CopyIncrementProposerPriority(1)
nextState.ConsensusParams = *newParams
nextState.LastHeightConsensusParamsChanged = nextHeight + 1
nextState.LastHeightValidatorsChanged = nextHeight + 1
// update the state
require.NoError(t, stateStore.Save(nextState))
block := &types.BlockMeta{
BlockID: initialState.LastBlockID,
Header: types.Header{
Height: initialState.LastBlockHeight,
AppHash: initialState.AppHash,
LastBlockID: makeBlockIDRandom(),
LastResultsHash: initialState.LastResultsHash,
},
}
blockStore.On("LoadBlockMeta", initialState.LastBlockHeight).Return(block)
blockStore.On("Height").Return(nextHeight)
// rollback the state
rollbackHeight, rollbackHash, err := state.Rollback(blockStore, stateStore)
require.NoError(t, err)
require.EqualValues(t, height, rollbackHeight)
require.EqualValues(t, initialState.AppHash, rollbackHash)
blockStore.AssertExpectations(t)
// assert that we've recovered the prior state
loadedState, err := stateStore.Load()
require.NoError(t, err)
require.EqualValues(t, initialState, loadedState)
}
func TestRollbackNoState(t *testing.T) {
stateStore := state.NewStore(dbm.NewMemDB())
blockStore := &mocks.BlockStore{}
var (
height int64 = 100
appVersion uint64 = 10
)
_, _, err := state.Rollback(blockStore, stateStore)
require.Error(t, err)
require.Contains(t, err.Error(), "no state found")
}
func TestRollbackNoBlocks(t *testing.T) {
const height = int64(100)
stateStore := setupStateStore(t, height)
blockStore := &mocks.BlockStore{}
blockStore.On("Height").Return(height)
blockStore.On("LoadBlockMeta", height-1).Return(nil)
_, _, err := state.Rollback(blockStore, stateStore)
require.Error(t, err)
require.Contains(t, err.Error(), "block at height 99 not found")
}
func TestRollbackDifferentStateHeight(t *testing.T) {
const height = int64(100)
stateStore := setupStateStore(t, height)
blockStore := &mocks.BlockStore{}
blockStore.On("Height").Return(height + 2)
_, _, err := state.Rollback(blockStore, stateStore)
require.Error(t, err)
require.Equal(t, err.Error(), "statestore height (100) is not one below or equal to blockstore height (102)")
}
func setupStateStore(t *testing.T, height int64) state.Store {
stateStore := state.NewStore(dbm.NewMemDB())
valSet, _ := types.RandValidatorSet(5, 10)
params := types.DefaultConsensusParams()
params.Version.AppVersion = appVersion
newParams := types.DefaultConsensusParams()
newParams.Block.MaxBytes = 10000
params.Version.AppVersion = 10
initialState := state.State{
Version: tmstate.Version{
@@ -53,99 +131,7 @@ func TestRollback(t *testing.T) {
LastHeightConsensusParamsChanged: height + 1,
}
require.NoError(t, stateStore.Bootstrap(initialState))
height++
block := &types.BlockMeta{
Header: types.Header{
Height: height,
AppHash: initialState.AppHash,
LastBlockID: initialState.LastBlockID,
LastResultsHash: initialState.LastResultsHash,
},
}
blockStore.On("LoadBlockMeta", height).Return(block)
appVersion++
newParams.Version.AppVersion = appVersion
nextState := initialState.Copy()
nextState.LastBlockHeight = height
nextState.Version.Consensus.App = appVersion
nextState.LastBlockID = makeBlockIDRandom()
nextState.AppHash = tmhash.Sum([]byte("next_app_hash"))
nextState.LastValidators = initialState.Validators
nextState.Validators = initialState.NextValidators
nextState.NextValidators = initialState.NextValidators.CopyIncrementProposerPriority(1)
nextState.ConsensusParams = *newParams
nextState.LastHeightConsensusParamsChanged = height + 1
nextState.LastHeightValidatorsChanged = height + 1
// update the state
require.NoError(t, stateStore.Save(nextState))
// rollback the state
rollbackHeight, rollbackHash, err := state.Rollback(blockStore, stateStore)
require.NoError(t, err)
require.EqualValues(t, int64(100), rollbackHeight)
require.EqualValues(t, initialState.AppHash, rollbackHash)
blockStore.AssertExpectations(t)
// assert that we've recovered the prior state
loadedState, err := stateStore.Load()
require.NoError(t, err)
require.EqualValues(t, initialState, loadedState)
}
func TestRollbackNoState(t *testing.T) {
stateStore := state.NewStore(dbm.NewMemDB())
blockStore := &mocks.BlockStore{}
_, _, err := state.Rollback(blockStore, stateStore)
require.Error(t, err)
require.Contains(t, err.Error(), "no state found")
}
func TestRollbackNoBlocks(t *testing.T) {
stateStore := state.NewStore(dbm.NewMemDB())
blockStore := &mocks.BlockStore{}
var (
height int64 = 100
appVersion uint64 = 10
)
valSet, _ := types.RandValidatorSet(5, 10)
params := types.DefaultConsensusParams()
params.Version.AppVersion = appVersion
newParams := types.DefaultConsensusParams()
newParams.Block.MaxBytes = 10000
initialState := state.State{
Version: tmstate.Version{
Consensus: tmversion.Consensus{
Block: version.BlockProtocol,
App: 10,
},
Software: version.TMCoreSemVer,
},
ChainID: "test-chain",
InitialHeight: 10,
LastBlockID: makeBlockIDRandom(),
AppHash: tmhash.Sum([]byte("app_hash")),
LastResultsHash: tmhash.Sum([]byte("last_results_hash")),
LastBlockHeight: height,
LastValidators: valSet,
Validators: valSet.CopyIncrementProposerPriority(1),
NextValidators: valSet.CopyIncrementProposerPriority(2),
LastHeightValidatorsChanged: height + 1,
ConsensusParams: *params,
LastHeightConsensusParamsChanged: height + 1,
}
require.NoError(t, stateStore.Save(initialState))
blockStore.On("LoadBlockMeta", height).Return(nil)
_, _, err := state.Rollback(blockStore, stateStore)
require.Error(t, err)
require.Contains(t, err.Error(), "block at height 100 not found")
return stateStore
}
func makeBlockIDRandom() types.BlockID {
+6
View File
@@ -68,6 +68,8 @@ type Store interface {
Bootstrap(State) error
// PruneStates takes the height from which to start prning and which height stop at
PruneStates(int64, int64) error
// Close closes the connection with the database
Close() error
}
// dbStore wraps a db (github.com/tendermint/tm-db)
@@ -593,3 +595,7 @@ func (store dbStore) saveConsensusParamsInfo(nextHeight, changeHeight int64, par
return nil
}
func (store dbStore) Close() error {
return store.db.Close()
}