@ -0,0 +1,46 @@ | |||||
package commands | |||||
import ( | |||||
"fmt" | |||||
"github.com/spf13/cobra" | |||||
cfg "github.com/tendermint/tendermint/config" | |||||
"github.com/tendermint/tendermint/internal/state" | |||||
) | |||||
var RollbackStateCmd = &cobra.Command{ | |||||
Use: "rollback", | |||||
Short: "rollback tendermint state by one height", | |||||
Long: ` | |||||
A state rollback is performed to recover from an incorrect application state transition, | |||||
when Tendermint has persisted an incorrect app hash and is thus unable to make | |||||
progress. Rollback overwrites a state at height n with the state at height n - 1. | |||||
The application should also roll back to height n - 1. No blocks are removed, so upon | |||||
restarting Tendermint the transactions in block n will be re-executed against the | |||||
application. | |||||
`, | |||||
RunE: func(cmd *cobra.Command, args []string) error { | |||||
height, hash, err := RollbackState(config) | |||||
if err != nil { | |||||
return fmt.Errorf("failed to rollback state: %w", err) | |||||
} | |||||
fmt.Printf("Rolled back state to height %d and hash %v", height, hash) | |||||
return nil | |||||
}, | |||||
} | |||||
// RollbackState takes the state at the current height n and overwrites it with the state | |||||
// at height n - 1. Note state here refers to tendermint state not application state. | |||||
// Returns the latest state height and app hash alongside an error if there was one. | |||||
func RollbackState(config *cfg.Config) (int64, []byte, error) { | |||||
// use the parsed config to load the block and state store | |||||
blockStore, stateStore, err := loadStateAndBlockStore(config) | |||||
if err != nil { | |||||
return -1, nil, err | |||||
} | |||||
// rollback the last state | |||||
return state.Rollback(blockStore, stateStore) | |||||
} |
@ -0,0 +1,87 @@ | |||||
package state | |||||
import ( | |||||
"errors" | |||||
"fmt" | |||||
"github.com/tendermint/tendermint/version" | |||||
) | |||||
// Rollback overwrites the current Tendermint state (height n) with the most | |||||
// recent previous state (height n - 1). | |||||
// Note that this function does not affect application state. | |||||
func Rollback(bs BlockStore, ss Store) (int64, []byte, error) { | |||||
invalidState, err := ss.Load() | |||||
if err != nil { | |||||
return -1, nil, err | |||||
} | |||||
if invalidState.IsEmpty() { | |||||
return -1, nil, errors.New("no state found") | |||||
} | |||||
rollbackHeight := invalidState.LastBlockHeight | |||||
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) | |||||
if err != nil { | |||||
return -1, nil, err | |||||
} | |||||
previousParams, err := ss.LoadConsensusParams(rollbackHeight) | |||||
if err != nil { | |||||
return -1, nil, err | |||||
} | |||||
valChangeHeight := invalidState.LastHeightValidatorsChanged | |||||
// this can only happen if the validator set changed since the last block | |||||
if valChangeHeight > rollbackHeight { | |||||
valChangeHeight = rollbackHeight | |||||
} | |||||
paramsChangeHeight := invalidState.LastHeightConsensusParamsChanged | |||||
// this can only happen if params changed from the last block | |||||
if paramsChangeHeight > rollbackHeight { | |||||
paramsChangeHeight = rollbackHeight | |||||
} | |||||
// build the new state from the old state and the prior block | |||||
rolledBackState := State{ | |||||
Version: Version{ | |||||
Consensus: version.Consensus{ | |||||
Block: version.BlockProtocol, | |||||
App: previousParams.Version.AppVersion, | |||||
}, | |||||
Software: version.TMVersion, | |||||
}, | |||||
// immutable fields | |||||
ChainID: invalidState.ChainID, | |||||
InitialHeight: invalidState.InitialHeight, | |||||
LastBlockHeight: invalidState.LastBlockHeight - 1, | |||||
LastBlockID: rollbackBlock.Header.LastBlockID, | |||||
LastBlockTime: rollbackBlock.Header.Time, | |||||
NextValidators: invalidState.Validators, | |||||
Validators: invalidState.LastValidators, | |||||
LastValidators: previousValidatorSet, | |||||
LastHeightValidatorsChanged: valChangeHeight, | |||||
ConsensusParams: previousParams, | |||||
LastHeightConsensusParamsChanged: paramsChangeHeight, | |||||
LastResultsHash: rollbackBlock.Header.LastResultsHash, | |||||
AppHash: rollbackBlock.Header.AppHash, | |||||
} | |||||
// persist the new state. This overrides the invalid one. NOTE: this will also | |||||
// persist the validator set and consensus params over the existing structures, | |||||
// but both should be the same | |||||
if err := ss.Save(rolledBackState); err != nil { | |||||
return -1, nil, fmt.Errorf("failed to save rolled back state: %w", err) | |||||
} | |||||
return rolledBackState.LastBlockHeight, rolledBackState.AppHash, nil | |||||
} |
@ -0,0 +1,146 @@ | |||||
package state_test | |||||
import ( | |||||
"testing" | |||||
"github.com/stretchr/testify/require" | |||||
dbm "github.com/tendermint/tm-db" | |||||
"github.com/tendermint/tendermint/internal/state" | |||||
"github.com/tendermint/tendermint/internal/state/mocks" | |||||
"github.com/tendermint/tendermint/internal/test/factory" | |||||
"github.com/tendermint/tendermint/types" | |||||
"github.com/tendermint/tendermint/version" | |||||
) | |||||
func TestRollback(t *testing.T) { | |||||
stateStore := state.NewStore(dbm.NewMemDB()) | |||||
blockStore := &mocks.BlockStore{} | |||||
var ( | |||||
height int64 = 100 | |||||
appVersion uint64 = 10 | |||||
) | |||||
valSet, _ := factory.RandValidatorSet(5, 10) | |||||
params := types.DefaultConsensusParams() | |||||
params.Version.AppVersion = appVersion | |||||
newParams := types.DefaultConsensusParams() | |||||
newParams.Block.MaxBytes = 10000 | |||||
initialState := state.State{ | |||||
Version: state.Version{ | |||||
Consensus: version.Consensus{ | |||||
Block: version.BlockProtocol, | |||||
App: 10, | |||||
}, | |||||
Software: version.TMVersion, | |||||
}, | |||||
ChainID: factory.DefaultTestChainID, | |||||
InitialHeight: 10, | |||||
LastBlockID: factory.MakeBlockID(), | |||||
AppHash: factory.RandomHash(), | |||||
LastResultsHash: factory.RandomHash(), | |||||
LastBlockHeight: height, | |||||
LastValidators: valSet, | |||||
Validators: valSet.CopyIncrementProposerPriority(1), | |||||
NextValidators: valSet.CopyIncrementProposerPriority(2), | |||||
LastHeightValidatorsChanged: height + 1, | |||||
ConsensusParams: *params, | |||||
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 = factory.MakeBlockID() | |||||
nextState.AppHash = factory.RandomHash() | |||||
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, _ := factory.RandValidatorSet(5, 10) | |||||
params := types.DefaultConsensusParams() | |||||
params.Version.AppVersion = appVersion | |||||
newParams := types.DefaultConsensusParams() | |||||
newParams.Block.MaxBytes = 10000 | |||||
initialState := state.State{ | |||||
Version: state.Version{ | |||||
Consensus: version.Consensus{ | |||||
Block: version.BlockProtocol, | |||||
App: 10, | |||||
}, | |||||
Software: version.TMVersion, | |||||
}, | |||||
ChainID: factory.DefaultTestChainID, | |||||
InitialHeight: 10, | |||||
LastBlockID: factory.MakeBlockID(), | |||||
AppHash: factory.RandomHash(), | |||||
LastResultsHash: factory.RandomHash(), | |||||
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") | |||||
} |