package state import ( "errors" "fmt" tmstate "github.com/tendermint/tendermint/proto/tendermint/state" tmversion "github.com/tendermint/tendermint/proto/tendermint/version" "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") } 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) } // We also need to retrieve the latest block because the app hash and last // results hash is only agreed upon in the following block. latestBlock := bs.LoadBlockMeta(invalidState.LastBlockHeight) if latestBlock == nil { return -1, nil, fmt.Errorf("block at height %d not found", invalidState.LastBlockHeight) } previousLastValidatorSet, err := ss.LoadValidators(rollbackHeight) if err != nil { return -1, nil, err } previousParams, err := ss.LoadConsensusParams(rollbackHeight + 1) 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 + 1 } paramsChangeHeight := invalidState.LastHeightConsensusParamsChanged // this can only happen if params changed from the last block if paramsChangeHeight > rollbackHeight { paramsChangeHeight = rollbackHeight + 1 } // build the new state from the old state and the prior block rolledBackState := State{ Version: tmstate.Version{ Consensus: tmversion.Consensus{ Block: version.BlockProtocol, App: previousParams.Version.AppVersion, }, Software: version.TMCoreSemVer, }, // immutable fields ChainID: invalidState.ChainID, InitialHeight: invalidState.InitialHeight, LastBlockHeight: rollbackBlock.Header.Height, LastBlockID: rollbackBlock.BlockID, LastBlockTime: rollbackBlock.Header.Time, NextValidators: invalidState.Validators, Validators: invalidState.LastValidators, LastValidators: previousLastValidatorSet, LastHeightValidatorsChanged: valChangeHeight, ConsensusParams: previousParams, LastHeightConsensusParamsChanged: paramsChangeHeight, LastResultsHash: latestBlock.Header.LastResultsHash, AppHash: latestBlock.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 }