github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/consensus/replay.go (about)

     1  package consensus
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"hash/crc32"
     8  	"io"
     9  	"reflect"
    10  	"time"
    11  
    12  	abci "github.com/badrootd/nibiru-cometbft/abci/types"
    13  	"github.com/badrootd/nibiru-cometbft/crypto/merkle"
    14  	"github.com/badrootd/nibiru-cometbft/libs/log"
    15  	"github.com/badrootd/nibiru-cometbft/proxy"
    16  	sm "github.com/badrootd/nibiru-cometbft/state"
    17  	"github.com/badrootd/nibiru-cometbft/types"
    18  )
    19  
    20  var crc32c = crc32.MakeTable(crc32.Castagnoli)
    21  
    22  // Functionality to replay blocks and messages on recovery from a crash.
    23  // There are two general failure scenarios:
    24  //
    25  //  1. failure during consensus
    26  //  2. failure while applying the block
    27  //
    28  // The former is handled by the WAL, the latter by the proxyApp Handshake on
    29  // restart, which ultimately hands off the work to the WAL.
    30  
    31  //-----------------------------------------
    32  // 1. Recover from failure during consensus
    33  // (by replaying messages from the WAL)
    34  //-----------------------------------------
    35  
    36  // Unmarshal and apply a single message to the consensus state as if it were
    37  // received in receiveRoutine.  Lines that start with "#" are ignored.
    38  // NOTE: receiveRoutine should not be running.
    39  func (cs *State) readReplayMessage(msg *TimedWALMessage, newStepSub types.Subscription) error {
    40  	// Skip meta messages which exist for demarcating boundaries.
    41  	if _, ok := msg.Msg.(EndHeightMessage); ok {
    42  		return nil
    43  	}
    44  
    45  	// for logging
    46  	switch m := msg.Msg.(type) {
    47  	case types.EventDataRoundState:
    48  		cs.Logger.Info("Replay: New Step", "height", m.Height, "round", m.Round, "step", m.Step)
    49  		// these are playback checks
    50  		ticker := time.After(time.Second * 2)
    51  		if newStepSub != nil {
    52  			select {
    53  			case stepMsg := <-newStepSub.Out():
    54  				m2 := stepMsg.Data().(types.EventDataRoundState)
    55  				if m.Height != m2.Height || m.Round != m2.Round || m.Step != m2.Step {
    56  					return fmt.Errorf("roundState mismatch. Got %v; Expected %v", m2, m)
    57  				}
    58  			case <-newStepSub.Cancelled():
    59  				return fmt.Errorf("failed to read off newStepSub.Out(). newStepSub was canceled")
    60  			case <-ticker:
    61  				return fmt.Errorf("failed to read off newStepSub.Out()")
    62  			}
    63  		}
    64  	case msgInfo:
    65  		peerID := m.PeerID
    66  		if peerID == "" {
    67  			peerID = "local"
    68  		}
    69  		switch msg := m.Msg.(type) {
    70  		case *ProposalMessage:
    71  			p := msg.Proposal
    72  			cs.Logger.Info("Replay: Proposal", "height", p.Height, "round", p.Round, "header",
    73  				p.BlockID.PartSetHeader, "pol", p.POLRound, "peer", peerID)
    74  		case *BlockPartMessage:
    75  			cs.Logger.Info("Replay: BlockPart", "height", msg.Height, "round", msg.Round, "peer", peerID)
    76  		case *VoteMessage:
    77  			v := msg.Vote
    78  			cs.Logger.Info("Replay: Vote", "height", v.Height, "round", v.Round, "type", v.Type,
    79  				"blockID", v.BlockID, "peer", peerID)
    80  		}
    81  
    82  		cs.handleMsg(m)
    83  	case timeoutInfo:
    84  		cs.Logger.Info("Replay: Timeout", "height", m.Height, "round", m.Round, "step", m.Step, "dur", m.Duration)
    85  		cs.handleTimeout(m, cs.RoundState)
    86  	default:
    87  		return fmt.Errorf("replay: Unknown TimedWALMessage type: %v", reflect.TypeOf(msg.Msg))
    88  	}
    89  	return nil
    90  }
    91  
    92  // Replay only those messages since the last block.  `timeoutRoutine` should
    93  // run concurrently to read off tickChan.
    94  func (cs *State) catchupReplay(csHeight int64) error {
    95  
    96  	// Set replayMode to true so we don't log signing errors.
    97  	cs.replayMode = true
    98  	defer func() { cs.replayMode = false }()
    99  
   100  	// Ensure that #ENDHEIGHT for this height doesn't exist.
   101  	// NOTE: This is just a sanity check. As far as we know things work fine
   102  	// without it, and Handshake could reuse State if it weren't for
   103  	// this check (since we can crash after writing #ENDHEIGHT).
   104  	//
   105  	// Ignore data corruption errors since this is a sanity check.
   106  	gr, found, err := cs.wal.SearchForEndHeight(csHeight, &WALSearchOptions{IgnoreDataCorruptionErrors: true})
   107  	if err != nil {
   108  		return err
   109  	}
   110  	if gr != nil {
   111  		if err := gr.Close(); err != nil {
   112  			return err
   113  		}
   114  	}
   115  	if found {
   116  		return fmt.Errorf("wal should not contain #ENDHEIGHT %d", csHeight)
   117  	}
   118  
   119  	// Search for last height marker.
   120  	//
   121  	// Ignore data corruption errors in previous heights because we only care about last height
   122  	if csHeight < cs.state.InitialHeight {
   123  		return fmt.Errorf("cannot replay height %v, below initial height %v", csHeight, cs.state.InitialHeight)
   124  	}
   125  	endHeight := csHeight - 1
   126  	if csHeight == cs.state.InitialHeight {
   127  		endHeight = 0
   128  	}
   129  	gr, found, err = cs.wal.SearchForEndHeight(endHeight, &WALSearchOptions{IgnoreDataCorruptionErrors: true})
   130  	if err == io.EOF {
   131  		cs.Logger.Error("Replay: wal.group.Search returned EOF", "#ENDHEIGHT", endHeight)
   132  	} else if err != nil {
   133  		return err
   134  	}
   135  	if !found {
   136  		return fmt.Errorf("cannot replay height %d. WAL does not contain #ENDHEIGHT for %d", csHeight, endHeight)
   137  	}
   138  	defer gr.Close()
   139  
   140  	cs.Logger.Info("Catchup by replaying consensus messages", "height", csHeight)
   141  
   142  	var msg *TimedWALMessage
   143  	dec := WALDecoder{gr}
   144  
   145  LOOP:
   146  	for {
   147  		msg, err = dec.Decode()
   148  		switch {
   149  		case err == io.EOF:
   150  			break LOOP
   151  		case IsDataCorruptionError(err):
   152  			cs.Logger.Error("data has been corrupted in last height of consensus WAL", "err", err, "height", csHeight)
   153  			return err
   154  		case err != nil:
   155  			return err
   156  		}
   157  
   158  		// NOTE: since the priv key is set when the msgs are received
   159  		// it will attempt to eg double sign but we can just ignore it
   160  		// since the votes will be replayed and we'll get to the next step
   161  		if err := cs.readReplayMessage(msg, nil); err != nil {
   162  			return err
   163  		}
   164  	}
   165  	cs.Logger.Info("Replay: Done")
   166  	return nil
   167  }
   168  
   169  //--------------------------------------------------------------------------------
   170  
   171  // Parses marker lines of the form:
   172  // #ENDHEIGHT: 12345
   173  /*
   174  func makeHeightSearchFunc(height int64) auto.SearchFunc {
   175  	return func(line string) (int, error) {
   176  		line = strings.TrimRight(line, "\n")
   177  		parts := strings.Split(line, " ")
   178  		if len(parts) != 2 {
   179  			return -1, errors.New("line did not have 2 parts")
   180  		}
   181  		i, err := strconv.Atoi(parts[1])
   182  		if err != nil {
   183  			return -1, errors.New("failed to parse INFO: " + err.Error())
   184  		}
   185  		if height < i {
   186  			return 1, nil
   187  		} else if height == i {
   188  			return 0, nil
   189  		} else {
   190  			return -1, nil
   191  		}
   192  	}
   193  }*/
   194  
   195  //---------------------------------------------------
   196  // 2. Recover from failure while applying the block.
   197  // (by handshaking with the app to figure out where
   198  // we were last, and using the WAL to recover there.)
   199  //---------------------------------------------------
   200  
   201  type Handshaker struct {
   202  	stateStore   sm.Store
   203  	initialState sm.State
   204  	store        sm.BlockStore
   205  	eventBus     types.BlockEventPublisher
   206  	genDoc       *types.GenesisDoc
   207  	logger       log.Logger
   208  
   209  	nBlocks int // number of blocks applied to the state
   210  }
   211  
   212  func NewHandshaker(stateStore sm.Store, state sm.State,
   213  	store sm.BlockStore, genDoc *types.GenesisDoc) *Handshaker {
   214  
   215  	return &Handshaker{
   216  		stateStore:   stateStore,
   217  		initialState: state,
   218  		store:        store,
   219  		eventBus:     types.NopEventBus{},
   220  		genDoc:       genDoc,
   221  		logger:       log.NewNopLogger(),
   222  		nBlocks:      0,
   223  	}
   224  }
   225  
   226  func (h *Handshaker) SetLogger(l log.Logger) {
   227  	h.logger = l
   228  }
   229  
   230  // SetEventBus - sets the event bus for publishing block related events.
   231  // If not called, it defaults to types.NopEventBus.
   232  func (h *Handshaker) SetEventBus(eventBus types.BlockEventPublisher) {
   233  	h.eventBus = eventBus
   234  }
   235  
   236  // NBlocks returns the number of blocks applied to the state.
   237  func (h *Handshaker) NBlocks() int {
   238  	return h.nBlocks
   239  }
   240  
   241  // TODO: retry the handshake/replay if it fails ?
   242  func (h *Handshaker) Handshake(proxyApp proxy.AppConns) error {
   243  	return h.HandshakeWithContext(context.TODO(), proxyApp)
   244  }
   245  
   246  // HandshakeWithContext is cancellable version of Handshake
   247  func (h *Handshaker) HandshakeWithContext(ctx context.Context, proxyApp proxy.AppConns) error {
   248  
   249  	// Handshake is done via ABCI Info on the query conn.
   250  	res, err := proxyApp.Query().InfoSync(proxy.RequestInfo)
   251  	if err != nil {
   252  		return fmt.Errorf("error calling Info: %v", err)
   253  	}
   254  
   255  	blockHeight := res.LastBlockHeight
   256  	if blockHeight < 0 {
   257  		return fmt.Errorf("got a negative last block height (%d) from the app", blockHeight)
   258  	}
   259  	appHash := res.LastBlockAppHash
   260  
   261  	h.logger.Info("ABCI Handshake App Info",
   262  		"height", blockHeight,
   263  		"hash", appHash,
   264  		"software-version", res.Version,
   265  		"protocol-version", res.AppVersion,
   266  	)
   267  
   268  	// Only set the version if there is no existing state.
   269  	if h.initialState.LastBlockHeight == 0 {
   270  		h.initialState.Version.Consensus.App = res.AppVersion
   271  	}
   272  
   273  	// Replay blocks up to the latest in the blockstore.
   274  	appHash, err = h.ReplayBlocksWithContext(ctx, h.initialState, appHash, blockHeight, proxyApp)
   275  	if err != nil {
   276  		return fmt.Errorf("error on replay: %v", err)
   277  	}
   278  
   279  	h.logger.Info("Completed ABCI Handshake - CometBFT and App are synced",
   280  		"appHeight", blockHeight, "appHash", appHash)
   281  
   282  	// TODO: (on restart) replay mempool
   283  
   284  	return nil
   285  }
   286  
   287  // ReplayBlocks replays all blocks since appBlockHeight and ensures the result
   288  // matches the current state.
   289  // Returns the final AppHash or an error.
   290  func (h *Handshaker) ReplayBlocks(
   291  	state sm.State,
   292  	appHash []byte,
   293  	appBlockHeight int64,
   294  	proxyApp proxy.AppConns,
   295  ) ([]byte, error) {
   296  	return h.ReplayBlocksWithContext(context.TODO(), state, appHash, appBlockHeight, proxyApp)
   297  }
   298  
   299  // ReplayBlocksWithContext is cancellable version of ReplayBlocks.
   300  func (h *Handshaker) ReplayBlocksWithContext(
   301  	ctx context.Context,
   302  	state sm.State,
   303  	appHash []byte,
   304  	appBlockHeight int64,
   305  	proxyApp proxy.AppConns,
   306  ) ([]byte, error) {
   307  	storeBlockBase := h.store.Base()
   308  	storeBlockHeight := h.store.Height()
   309  	stateBlockHeight := state.LastBlockHeight
   310  	h.logger.Info(
   311  		"ABCI Replay Blocks",
   312  		"appHeight",
   313  		appBlockHeight,
   314  		"storeHeight",
   315  		storeBlockHeight,
   316  		"stateHeight",
   317  		stateBlockHeight)
   318  
   319  	// If appBlockHeight == 0 it means that we are at genesis and hence should send InitChain.
   320  	if appBlockHeight == 0 {
   321  		validators := make([]*types.Validator, len(h.genDoc.Validators))
   322  		for i, val := range h.genDoc.Validators {
   323  			validators[i] = types.NewValidator(val.PubKey, val.Power)
   324  		}
   325  		validatorSet := types.NewValidatorSet(validators)
   326  		nextVals := types.TM2PB.ValidatorUpdates(validatorSet)
   327  		pbparams := h.genDoc.ConsensusParams.ToProto()
   328  		req := abci.RequestInitChain{
   329  			Time:            h.genDoc.GenesisTime,
   330  			ChainId:         h.genDoc.ChainID,
   331  			InitialHeight:   h.genDoc.InitialHeight,
   332  			ConsensusParams: &pbparams,
   333  			Validators:      nextVals,
   334  			AppStateBytes:   h.genDoc.AppState,
   335  		}
   336  		res, err := proxyApp.Consensus().InitChainSync(req)
   337  		if err != nil {
   338  			return nil, err
   339  		}
   340  
   341  		appHash = res.AppHash
   342  
   343  		if stateBlockHeight == 0 { // we only update state when we are in initial state
   344  			// If the app did not return an app hash, we keep the one set from the genesis doc in
   345  			// the state. We don't set appHash since we don't want the genesis doc app hash
   346  			// recorded in the genesis block. We should probably just remove GenesisDoc.AppHash.
   347  			if len(res.AppHash) > 0 {
   348  				state.AppHash = res.AppHash
   349  			}
   350  			// If the app returned validators or consensus params, update the state.
   351  			if len(res.Validators) > 0 {
   352  				vals, err := types.PB2TM.ValidatorUpdates(res.Validators)
   353  				if err != nil {
   354  					return nil, err
   355  				}
   356  				state.Validators = types.NewValidatorSet(vals)
   357  				state.NextValidators = types.NewValidatorSet(vals).CopyIncrementProposerPriority(1)
   358  			} else if len(h.genDoc.Validators) == 0 {
   359  				// If validator set is not set in genesis and still empty after InitChain, exit.
   360  				return nil, fmt.Errorf("validator set is nil in genesis and still empty after InitChain")
   361  			}
   362  
   363  			if res.ConsensusParams != nil {
   364  				state.ConsensusParams = state.ConsensusParams.Update(res.ConsensusParams)
   365  				state.Version.Consensus.App = state.ConsensusParams.Version.App
   366  			}
   367  			// We update the last results hash with the empty hash, to conform with RFC-6962.
   368  			state.LastResultsHash = merkle.HashFromByteSlices(nil)
   369  			if err := h.stateStore.Save(state); err != nil {
   370  				return nil, err
   371  			}
   372  		}
   373  	}
   374  
   375  	// First handle edge cases and constraints on the storeBlockHeight and storeBlockBase.
   376  	switch {
   377  	case storeBlockHeight == 0:
   378  		assertAppHashEqualsOneFromState(appHash, state)
   379  		return appHash, nil
   380  
   381  	case appBlockHeight == 0 && state.InitialHeight < storeBlockBase:
   382  		// the app has no state, and the block store is truncated above the initial height
   383  		return appHash, sm.ErrAppBlockHeightTooLow{AppHeight: appBlockHeight, StoreBase: storeBlockBase}
   384  
   385  	case appBlockHeight > 0 && appBlockHeight < storeBlockBase-1:
   386  		// the app is too far behind truncated store (can be 1 behind since we replay the next)
   387  		return appHash, sm.ErrAppBlockHeightTooLow{AppHeight: appBlockHeight, StoreBase: storeBlockBase}
   388  
   389  	case storeBlockHeight < appBlockHeight:
   390  		// the app should never be ahead of the store (but this is under app's control)
   391  		return appHash, sm.ErrAppBlockHeightTooHigh{CoreHeight: storeBlockHeight, AppHeight: appBlockHeight}
   392  
   393  	case storeBlockHeight < stateBlockHeight:
   394  		// the state should never be ahead of the store (this is under CometBFT's control)
   395  		panic(fmt.Sprintf("StateBlockHeight (%d) > StoreBlockHeight (%d)", stateBlockHeight, storeBlockHeight))
   396  
   397  	case storeBlockHeight > stateBlockHeight+1:
   398  		// store should be at most one ahead of the state (this is under CometBFT's control)
   399  		panic(fmt.Sprintf("StoreBlockHeight (%d) > StateBlockHeight + 1 (%d)", storeBlockHeight, stateBlockHeight+1))
   400  	}
   401  
   402  	var err error
   403  	// Now either store is equal to state, or one ahead.
   404  	// For each, consider all cases of where the app could be, given app <= store
   405  	if storeBlockHeight == stateBlockHeight {
   406  		// CometBFT ran Commit and saved the state.
   407  		// Either the app is asking for replay, or we're all synced up.
   408  		if appBlockHeight < storeBlockHeight {
   409  			// the app is behind, so replay blocks, but no need to go through WAL (state is already synced to store)
   410  			return h.replayBlocks(ctx, state, proxyApp, appBlockHeight, storeBlockHeight, false)
   411  
   412  		} else if appBlockHeight == storeBlockHeight {
   413  			// We're good!
   414  			assertAppHashEqualsOneFromState(appHash, state)
   415  			return appHash, nil
   416  		}
   417  
   418  	} else if storeBlockHeight == stateBlockHeight+1 {
   419  		// We saved the block in the store but haven't updated the state,
   420  		// so we'll need to replay a block using the WAL.
   421  		switch {
   422  		case appBlockHeight < stateBlockHeight:
   423  			// the app is further behind than it should be, so replay blocks
   424  			// but leave the last block to go through the WAL
   425  			return h.replayBlocks(ctx, state, proxyApp, appBlockHeight, storeBlockHeight, true)
   426  
   427  		case appBlockHeight == stateBlockHeight:
   428  			// We haven't run Commit (both the state and app are one block behind),
   429  			// so replayBlock with the real app.
   430  			// NOTE: We could instead use the cs.WAL on cs.Start,
   431  			// but we'd have to allow the WAL to replay a block that wrote it's #ENDHEIGHT
   432  			h.logger.Info("Replay last block using real app")
   433  			state, err = h.replayBlock(state, storeBlockHeight, proxyApp.Consensus())
   434  			return state.AppHash, err
   435  
   436  		case appBlockHeight == storeBlockHeight:
   437  			// We ran Commit, but didn't save the state, so replayBlock with mock app.
   438  			abciResponses, err := h.stateStore.LoadLastABCIResponse(storeBlockHeight)
   439  			if err != nil {
   440  				return nil, err
   441  			}
   442  			mockApp := newMockProxyApp(appHash, abciResponses)
   443  			h.logger.Info("Replay last block using mock app")
   444  			state, err = h.replayBlock(state, storeBlockHeight, mockApp)
   445  			return state.AppHash, err
   446  		}
   447  
   448  	}
   449  
   450  	panic(fmt.Sprintf("uncovered case! appHeight: %d, storeHeight: %d, stateHeight: %d",
   451  		appBlockHeight, storeBlockHeight, stateBlockHeight))
   452  }
   453  
   454  func (h *Handshaker) replayBlocks(
   455  	ctx context.Context,
   456  	state sm.State,
   457  	proxyApp proxy.AppConns,
   458  	appBlockHeight,
   459  	storeBlockHeight int64,
   460  	mutateState bool) ([]byte, error) {
   461  	// App is further behind than it should be, so we need to replay blocks.
   462  	// We replay all blocks from appBlockHeight+1.
   463  	//
   464  	// Note that we don't have an old version of the state,
   465  	// so we by-pass state validation/mutation using sm.ExecCommitBlock.
   466  	// This also means we won't be saving validator sets if they change during this period.
   467  	// TODO: Load the historical information to fix this and just use state.ApplyBlock
   468  	//
   469  	// If mutateState == true, the final block is replayed with h.replayBlock()
   470  
   471  	var appHash []byte
   472  	var err error
   473  	finalBlock := storeBlockHeight
   474  	if mutateState {
   475  		finalBlock--
   476  	}
   477  	firstBlock := appBlockHeight + 1
   478  	if firstBlock == 1 {
   479  		firstBlock = state.InitialHeight
   480  	}
   481  	for i := firstBlock; i <= finalBlock; i++ {
   482  		select {
   483  		case <-ctx.Done():
   484  			return nil, ctx.Err()
   485  		default:
   486  		}
   487  
   488  		h.logger.Info("Applying block", "height", i)
   489  		block := h.store.LoadBlock(i)
   490  		// Extra check to ensure the app was not changed in a way it shouldn't have.
   491  		if len(appHash) > 0 {
   492  			assertAppHashEqualsOneFromBlock(appHash, block)
   493  		}
   494  
   495  		appHash, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, h.logger, h.stateStore, h.genDoc.InitialHeight)
   496  		if err != nil {
   497  			return nil, err
   498  		}
   499  
   500  		h.nBlocks++
   501  	}
   502  
   503  	if mutateState {
   504  		// sync the final block
   505  		state, err = h.replayBlock(state, storeBlockHeight, proxyApp.Consensus())
   506  		if err != nil {
   507  			return nil, err
   508  		}
   509  		appHash = state.AppHash
   510  	}
   511  
   512  	assertAppHashEqualsOneFromState(appHash, state)
   513  	return appHash, nil
   514  }
   515  
   516  // ApplyBlock on the proxyApp with the last block.
   517  func (h *Handshaker) replayBlock(state sm.State, height int64, proxyApp proxy.AppConnConsensus) (sm.State, error) {
   518  	block := h.store.LoadBlock(height)
   519  	meta := h.store.LoadBlockMeta(height)
   520  
   521  	// Use stubs for both mempool and evidence pool since no transactions nor
   522  	// evidence are needed here - block already exists.
   523  	blockExec := sm.NewBlockExecutor(h.stateStore, h.logger, proxyApp, emptyMempool{}, sm.EmptyEvidencePool{})
   524  	blockExec.SetEventBus(h.eventBus)
   525  
   526  	var err error
   527  	state, _, err = blockExec.ApplyBlock(state, meta.BlockID, block)
   528  	if err != nil {
   529  		return sm.State{}, err
   530  	}
   531  
   532  	h.nBlocks++
   533  
   534  	return state, nil
   535  }
   536  
   537  func assertAppHashEqualsOneFromBlock(appHash []byte, block *types.Block) {
   538  	if !bytes.Equal(appHash, block.AppHash) {
   539  		panic(fmt.Sprintf(`block.AppHash does not match AppHash after replay. Got %X, expected %X.
   540  
   541  Block: %v
   542  `,
   543  			appHash, block.AppHash, block))
   544  	}
   545  }
   546  
   547  func assertAppHashEqualsOneFromState(appHash []byte, state sm.State) {
   548  	if !bytes.Equal(appHash, state.AppHash) {
   549  		panic(fmt.Sprintf(`state.AppHash does not match AppHash after replay. Got
   550  %X, expected %X.
   551  
   552  State: %v
   553  
   554  Did you reset CometBFT without resetting your application's data?`,
   555  			appHash, state.AppHash, state))
   556  	}
   557  }