Compare commits

...

4 Commits

Author SHA1 Message Date
4ac6c1defc cleaner test 2019-08-15 12:53:59 -07:00
d8aaaf63fb fix typos 2019-08-15 12:49:44 -07:00
7c11fc4116 complete reverting state and blockstore 2019-08-15 12:43:34 -07:00
22e2484878 start revert logic 2019-08-13 16:58:06 -07:00
8 changed files with 220 additions and 13 deletions

View File

@ -179,6 +179,25 @@ func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, b
return state, nil
}
// RevertBlock takes in a reverted block and the new latest block and returns the state
// to its previous State before reverted block got applied
// reverted block must be the LastBlockHeight of current state or will return error
// Will not revert changes to mempool or evpool
func (blockExec *BlockExecutor) RevertBlock(state State, reverted, newHead *types.Block) (State, error) {
if reverted.Header.Height != state.LastBlockHeight {
return state, fmt.Errorf("Can only revert latest block. Expected reverted block %d, got %d", state.LastBlockHeight, reverted.Header.Height)
}
prevState := blockExec.revertState(state, reverted, newHead)
// revert state to previous version
SaveState(blockExec.db, prevState)
// delete peripheral state info for reverted height
oldHeight := reverted.Header.Height
deleteABCIResponses(blockExec.db, oldHeight)
deleteConsensusParamsInfo(blockExec.db, oldHeight+1)
deleteValidatorsInfo(blockExec.db, oldHeight+2)
return prevState, nil
}
// Commit locks the mempool, runs the ABCI Commit message, and updates the
// mempool.
// It returns the result of calling abci.Commit (the AppHash), and an error.
@ -442,6 +461,40 @@ func updateState(
}, nil
}
func (blockExec *BlockExecutor) revertState(
state State,
reverted *types.Block,
newHead *types.Block,
) State {
lastValidatorsInfo := loadValidatorsInfo(blockExec.db, state.LastBlockHeight-1)
lastValidators, _ := LoadValidators(blockExec.db, lastValidatorsInfo.LastHeightChanged)
lastHeightValsChanged := lastValidatorsInfo.LastHeightChanged
lastConsParamsInfo := loadConsensusParamsInfo(blockExec.db, state.LastBlockHeight-1)
consParams, _ := LoadConsensusParams(blockExec.db, lastConsParamsInfo.LastHeightChanged)
lastHeightConsChanged := lastConsParamsInfo.LastHeightChanged
abciResponses, _ := LoadABCIResponses(blockExec.db, state.LastBlockHeight-1)
return State{
Version: state.Version,
ChainID: state.ChainID,
LastBlockHeight: state.LastBlockHeight - 1,
LastBlockTotalTx: state.LastBlockTotalTx - int64(len(reverted.Data.Txs)),
LastBlockID: reverted.Header.LastBlockID,
LastBlockTime: newHead.Header.Time,
NextValidators: state.Validators.Copy(),
Validators: state.LastValidators.Copy(),
LastValidators: lastValidators.Copy(),
LastHeightValidatorsChanged: lastHeightValsChanged,
ConsensusParams: consParams,
LastHeightConsensusParamsChanged: lastHeightConsChanged,
LastResultsHash: abciResponses.ResultsHash(),
AppHash: reverted.Header.AppHash,
}
}
// Fire NewBlock, NewBlockHeader.
// Fire TxEvent for every tx.
// NOTE: if Tendermint crashes before commit, some or all of these events may be published again.

View File

@ -47,6 +47,31 @@ func TestApplyBlock(t *testing.T) {
// TODO check state and mempool
}
func TestRevertBlock(t *testing.T) {
cc := proxy.NewLocalClientCreator(kvstore.NewKVStoreApplication())
proxyApp := proxy.NewAppConns(cc)
err := proxyApp.Start()
require.Nil(t, err)
defer proxyApp.Stop()
state, stateDB, privVals := makeState(3, 1)
blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(),
mock.Mempool{}, sm.MockEvidencePool{})
//nolint:ineffassign
state1, block1, _, commit, err := makeAndCommitGoodBlock(state, 1, &types.Commit{}, state.Validators.GetProposer().Address, blockExec, privVals, nil)
require.Nil(t, err)
state2, block2, _, _, err := makeAndCommitGoodBlock(state1, 2, commit, state1.Validators.GetProposer().Address, blockExec, privVals, nil)
require.Nil(t, err)
revertedState, _ := blockExec.RevertBlock(state2, block2, block1)
require.True(t, state1.Equals(revertedState), "Expected:\n%v\n\nActual:\n%v", state1, revertedState)
// TODO: check changes in ValSet and ConsensusParams
}
// TestBeginBlockValidators ensures we send absent validators list.
func TestBeginBlockValidators(t *testing.T) {
app := &testApp{}

View File

@ -40,33 +40,33 @@ func makeAndCommitGoodBlock(
proposerAddr []byte,
blockExec *sm.BlockExecutor,
privVals map[string]types.PrivValidator,
evidence []types.Evidence) (sm.State, types.BlockID, *types.Commit, error) {
evidence []types.Evidence) (sm.State, *types.Block, types.BlockID, *types.Commit, error) {
// A good block passes
state, blockID, err := makeAndApplyGoodBlock(state, height, lastCommit, proposerAddr, blockExec, evidence)
state, block, blockID, err := makeAndApplyGoodBlock(state, height, lastCommit, proposerAddr, blockExec, evidence)
if err != nil {
return state, types.BlockID{}, nil, err
return state, nil, types.BlockID{}, nil, err
}
// Simulate a lastCommit for this block from all validators for the next height
commit, err := makeValidCommit(height, blockID, state.Validators, privVals)
if err != nil {
return state, types.BlockID{}, nil, err
return state, nil, types.BlockID{}, nil, err
}
return state, blockID, commit, nil
return state, block, blockID, commit, nil
}
func makeAndApplyGoodBlock(state sm.State, height int64, lastCommit *types.Commit, proposerAddr []byte,
blockExec *sm.BlockExecutor, evidence []types.Evidence) (sm.State, types.BlockID, error) {
blockExec *sm.BlockExecutor, evidence []types.Evidence) (sm.State, *types.Block, types.BlockID, error) {
block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, evidence, proposerAddr)
if err := blockExec.ValidateBlock(state, block); err != nil {
return state, types.BlockID{}, err
return state, nil, types.BlockID{}, err
}
blockID := types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}
state, err := blockExec.ApplyBlock(state, blockID, block)
if err != nil {
return state, types.BlockID{}, err
return state, nil, types.BlockID{}, err
}
return state, blockID, nil
return state, block, blockID, nil
}
func makeValidCommit(height int64, blockID types.BlockID, vals *types.ValidatorSet, privVals map[string]types.PrivValidator) (*types.Commit, error) {

View File

@ -170,6 +170,12 @@ func saveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) {
db.SetSync(calcABCIResponsesKey(height), abciResponses.Bytes())
}
// DeleteABCIResponses should delete previously persisted ABCIResponses from database.
// Should only be called by RevertBlock
func deleteABCIResponses(db dbm.DB, height int64) {
db.DeleteSync(calcABCIResponsesKey(height))
}
//-----------------------------------------------------------------------------
// ValidatorsInfo represents the latest validator set, or the last height it changed
@ -261,6 +267,12 @@ func saveValidatorsInfo(db dbm.DB, height, lastHeightChanged int64, valSet *type
db.Set(calcValidatorsKey(height), valInfo.Bytes())
}
// DeleteValidatorsInfo should delete previously persisted ValidatorsInfo from database.
// Should only be called by RevertBlock
func deleteValidatorsInfo(db dbm.DB, height int64) {
db.DeleteSync(calcValidatorsKey(height))
}
//-----------------------------------------------------------------------------
// ConsensusParamsInfo represents the latest consensus params, or the last height it changed
@ -331,3 +343,9 @@ func saveConsensusParamsInfo(db dbm.DB, nextHeight, changeHeight int64, params t
}
db.Set(calcConsensusParamsKey(nextHeight), paramsInfo.Bytes())
}
// DeleteConsensusParamsInfo should delete previously persisted ConsesnsusParamsInfo from database.
// Should only be called by RevertBlock
func deleteConsensusParamsInfo(db dbm.DB, nextHeight int64) {
db.DeleteSync(calcConsensusParamsKey(nextHeight))
}

View File

@ -78,7 +78,7 @@ func TestValidateBlockHeader(t *testing.T) {
A good block passes
*/
var err error
state, _, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, nil)
state, _, _, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, nil)
require.NoError(t, err, "height %d", height)
}
}
@ -123,7 +123,7 @@ func TestValidateBlockCommit(t *testing.T) {
*/
var err error
var blockID types.BlockID
state, blockID, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, nil)
state, _, blockID, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, nil)
require.NoError(t, err, "height %d", height)
/*
@ -190,7 +190,7 @@ func TestValidateBlockEvidence(t *testing.T) {
}
var err error
state, _, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, evidence)
state, _, _, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, evidence)
require.NoError(t, err, "height %d", height)
}
}

View File

@ -195,6 +195,46 @@ func (bs *BlockStore) saveBlockPart(height int64, index int, part *types.Part) {
bs.db.Set(calcBlockPartKey(height, index), partBytes)
}
// RevertBlock reverts the store changes from the latest SaveBlock.
// Only modifies blockstore, does not change TM state
// For now, seenCommit for reverted block remains in db
// Returns the block that got reverted and the newest lastBlock
func (bs *BlockStore) RevertBlock() (reverted *types.Block, newHead *types.Block) {
bs.mtx.RLock()
latest := bs.height
bs.mtx.RUnlock()
// Load block to return later
reverted = bs.LoadBlock(latest)
newHead = bs.LoadBlock(latest - 1)
// Delete block meta
blockMeta := bs.LoadBlockMeta(latest)
blockID := blockMeta.BlockID
bs.db.Delete(calcBlockMetaKey(latest))
// Delete block parts
for i := 0; i < blockID.PartsHeader.Total; i++ {
bs.db.Delete(calcBlockPartKey(latest, i))
}
// Delete block commit
bs.db.Delete(calcBlockCommitKey(latest - 1))
// Update BlockStoreStateJSON descriptor
BlockStoreStateJSON{Height: latest - 1}.Save(bs.db)
// Update height
bs.mtx.Lock()
bs.height = latest - 1
bs.mtx.Unlock()
// Flush
bs.db.SetSync(nil, nil)
return
}
//-----------------------------------------------------------------------------
func calcBlockMetaKey(height int64) []byte {
@ -222,6 +262,11 @@ type BlockStoreStateJSON struct {
Height int64 `json:"height"`
}
// Returns blockStoreKey for testing purposes
func BlockStoreKey() []byte {
return blockStoreKey
}
// Save persists the blockStore state to the database as JSON.
func (bsj BlockStoreStateJSON) Save(db dbm.DB) {
bytes, err := cdc.MarshalJSON(bsj)

View File

@ -29,7 +29,7 @@ type cleanupFunc func()
// make a Commit with a single vote containing just the height and a timestamp
func makeTestCommit(height int64, timestamp time.Time) *types.Commit {
commitSigs := []*types.CommitSig{{Height: height, Timestamp: timestamp}}
commitSigs := []*types.CommitSig{{Type: types.PrecommitType, Height: height, Timestamp: timestamp}}
return types.NewCommit(types.BlockID{}, commitSigs)
}
@ -321,6 +321,56 @@ func TestBlockStoreSaveLoadBlock(t *testing.T) {
}
}
func TestRevertBlock(t *testing.T) {
state, bs, cleanup := makeStateAndBlockStore(log.NewTMLogger(new(bytes.Buffer)))
defer cleanup()
require.Equal(t, bs.Height(), int64(0), "initially the height should be zero")
// Create and Save first block
block1 := makeBlock(1, state, &types.Commit{})
partset1 := block1.MakePartSet(2)
seenCommit1 := makeTestCommit(1, tmtime.Now())
bs.SaveBlock(block1, partset1, seenCommit1)
// Sanity check: Loading first block returns correct value
recovered := bs.LoadBlock(1)
recovered.Data.Hash()
require.True(t, block1.Equals(recovered), fmt.Sprintf("Expected:\n%v\n\nActual:\n%v", block1, recovered))
// Create and save second block
header2 := types.Header{
Height: 2,
NumTxs: 100,
ChainID: "block_test",
Time: tmtime.Now(),
LastCommitHash: seenCommit1.Hash(),
}
block2 := newBlock(header2, seenCommit1)
partSet2 := block2.MakePartSet(2)
seenCommit2 := makeTestCommit(2, tmtime.Now())
bs.SaveBlock(block2, partSet2, seenCommit2)
// Check height is updated correctly
height := bs.Height()
require.Equal(t, int64(2), height)
// Revert latest block: block2
b2, b1 := bs.RevertBlock()
// Check height updated correctly
height = bs.Height()
require.Equal(t, int64(1), height)
b2.Data.Hash()
b1.Data.Hash()
// Check returned blocks are correct
require.True(t, block2.Equals(b2), "Expected:\n%v\n\nActual:%v\n", block2, b2)
require.True(t, block1.Equals(b1), "Expected:\n%v\nActual:%v\n", block1, b1)
}
func TestLoadBlockPart(t *testing.T) {
bs, db := freshBlockStore()
height, index := int64(10), 1

View File

@ -257,6 +257,22 @@ func (b *Block) StringShort() string {
return fmt.Sprintf("Block#%v", b.Hash())
}
// Equals returns whether two blocks are equal. Useful for testing
func (b *Block) Equals(b2 *Block) bool {
if b2 == nil {
return false
}
bz1, err := b.Marshal()
if err != nil {
return false
}
bz2, err := b2.Marshal()
if err != nil {
return false
}
return bytes.Equal(bz1, bz2)
}
//-----------------------------------------------------------
// These methods are for Protobuf Compatibility