diff --git a/cmd/tendermint/commands/rollback.go b/cmd/tendermint/commands/rollback.go index 5b933a7a7c5..a9324fe9c21 100644 --- a/cmd/tendermint/commands/rollback.go +++ b/cmd/tendermint/commands/rollback.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/spf13/cobra" + "github.com/spf13/viper" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/libs/os" "github.com/tendermint/tendermint/state" @@ -13,21 +14,24 @@ import ( db "github.com/tendermint/tm-db" ) +const flagForce = "force" + func MakeRollbackStateCommand() *cobra.Command { - confg := cfg.DefaultConfig() - return &cobra.Command{ + config := cfg.DefaultConfig() + cmd := &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. +The application should also roll back to height n - 1. Upon using the --force flag, tendermint +will rollback the state and delete the block n from the blockstore. This is useful when the +application returned a bad state upon processing the block n - 1. `, RunE: func(cmd *cobra.Command, args []string) error { - height, hash, err := RollbackState(confg) + forceRollback := viper.GetBool(flagForce) + height, hash, err := RollbackState(config, forceRollback) if err != nil { return fmt.Errorf("failed to rollback state: %w", err) } @@ -36,19 +40,26 @@ application. return nil }, } + + cmd.Flags().Bool(flagForce, false, "force rollback") + return cmd } // 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) { +func RollbackState(config *cfg.Config, forceRollback bool) (int64, []byte, error) { // use the parsed config to load the block and state store blocksdb, statedb, err := loadStateAndBlockStore(config) + defer func() { + blocksdb.Close() + statedb.Close() + }() if err != nil { return -1, nil, err } // rollback the last state - return rollbackState(blocksdb, statedb) + return rollbackState(blocksdb, statedb, forceRollback) } func loadStateAndBlockStore(cfg *cfg.Config) (db.DB, db.DB, error) { @@ -71,17 +82,17 @@ func loadStateAndBlockStore(cfg *cfg.Config) (db.DB, db.DB, error) { return blockStoreDB, stateDB, nil } -func rollbackState(blockStoreDB, stateDB db.DB) (int64, []byte, error) { +func rollbackState(blockStoreDB, stateDB db.DB, forceRollback bool) (int64, []byte, error) { blockStore := store.NewBlockStore(blockStoreDB) invalidState := state.LoadState(stateDB) height := blockStore.Height() - // skip - if height == invalidState.LastBlockHeight+1 { + // skip rollback if the state is already at the last block height + if height == invalidState.LastBlockHeight+1 && !forceRollback { return invalidState.LastBlockHeight, invalidState.AppHash, nil } - if height != invalidState.LastBlockHeight { + if height != invalidState.LastBlockHeight && !forceRollback { return -1, nil, fmt.Errorf("statestore height (%d) is not one below or equal to blockstore height (%d)", invalidState.LastBlockHeight, height) } @@ -153,5 +164,6 @@ func rollbackState(blockStoreDB, stateDB db.DB) (int64, []byte, error) { // saving the state state.SaveState(stateDB, rolledBackState) + blockStore.RemoveLatestBlock() return rolledBackState.LastBlockHeight, rolledBackState.AppHash, nil } diff --git a/store/store.go b/store/store.go index d8ec2f022d0..92c834f3844 100644 --- a/store/store.go +++ b/store/store.go @@ -188,6 +188,37 @@ func (bs *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, s bs.db.SetSync(nil, nil) } +// RemoveLatestBlock deletes the latest block from the store +// and rewinds the latest height by 1. +func (bs *BlockStore) RemoveLatestBlock() { + batch := bs.db.NewBatch() + defer batch.Close() + + bs.mtx.RLock() + height := bs.Height() + bs.mtx.RUnlock() + + blockMeta := bs.LoadBlockMeta(height) + // delete block parts + for i := 0; i < int(blockMeta.BlockID.PartsHeader.Total); i++ { + batch.Delete(calcBlockPartKey(height, i)) + } + + batch.Delete(calcBlockMetaKey(height)) + batch.Delete(calcBlockCommitKey(height)) + batch.Delete(calcSeenCommitKey(height)) + + // rollback BlockStoreStateJSON + BlockStoreStateJSON{Height: height - 1}.Save(bs.db) + + // rollback height + bs.mtx.Lock() + bs.height = height - 1 + bs.mtx.Unlock() + + batch.WriteSync() +} + func (bs *BlockStore) saveBlockPart(height int64, index int, part *types.Part) { if height != bs.Height()+1 { panic(fmt.Sprintf("BlockStore can only save contiguous blocks. Wanted %v, got %v", bs.Height()+1, height))