mirror of
https://github.com/tendermint/tendermint.git
synced 2026-07-20 15:02:33 +00:00
89bb82617a
When testing rollback feature in the Cosmos SDK, we found that the app hash
in Tendermint after rollback was the value after the latest block, rather than
before it.
Co-authored-by: Callum Waters <cmwaters19@gmail.com>
Co-authored-by: yihuang <huang@crypto.com>
(cherry picked from commit 8a238fdcb4)
Inline factory function that does not exist in this branch.
Co-authored-by: M. J. Fromberger <fromberger@interchain.io>
113 lines
3.9 KiB
Go
113 lines
3.9 KiB
Go
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
|
|
}
|