github.com/koko1123/flow-go-1@v0.29.6/module/mempool/consensus/exec_fork_suppressor.go (about)

     1  package consensus
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"sync"
     8  
     9  	"github.com/dgraph-io/badger/v3"
    10  	"github.com/rs/zerolog"
    11  	"github.com/rs/zerolog/log"
    12  	"go.uber.org/atomic"
    13  
    14  	"github.com/koko1123/flow-go-1/engine"
    15  	"github.com/koko1123/flow-go-1/model/flow"
    16  	"github.com/koko1123/flow-go-1/module/mempool"
    17  	"github.com/koko1123/flow-go-1/storage"
    18  	"github.com/koko1123/flow-go-1/storage/badger/operation"
    19  )
    20  
    21  // ExecForkSuppressor is a wrapper around a conventional mempool.IncorporatedResultSeals
    22  // mempool. It implements the following mitigation strategy for execution forks:
    23  //   - In case two conflicting results are considered sealable for the same block,
    24  //     sealing should halt. Specifically, two results are considered conflicting,
    25  //     if they differ in their start or end state.
    26  //   - Even after a restart, the sealing should not resume.
    27  //   - We rely on human intervention to resolve the conflict.
    28  //
    29  // The ExecForkSuppressor implements this mitigation strategy as follows:
    30  //   - For each candidate seal inserted into the mempool, indexes seal
    31  //     by respective blockID, storing all seals in the internal map `sealsForBlock`.
    32  //   - Whenever client perform any query, we check if there are conflicting seals.
    33  //   - We pick first seal available for a block and check whether
    34  //     the seal has the same state transition as other seals included for same block.
    35  //   - If conflicting state transitions for the same block are detected,
    36  //     ExecForkSuppressor sets an internal flag and thereafter
    37  //     reports the mempool as empty, which will lead to the respective
    38  //     consensus node not including any more seals.
    39  //   - Evidence for an execution fork stored in a database (persisted across restarts).
    40  //
    41  // Implementation is concurrency safe.
    42  type ExecForkSuppressor struct {
    43  	mutex            sync.RWMutex
    44  	seals            mempool.IncorporatedResultSeals
    45  	sealsForBlock    map[flow.Identifier]sealSet             // map BlockID -> set of IncorporatedResultSeal
    46  	byHeight         map[uint64]map[flow.Identifier]struct{} // map height -> set of executed block IDs at height
    47  	lowestHeight     uint64
    48  	execForkDetected atomic.Bool
    49  	onExecFork       ExecForkActor
    50  	db               *badger.DB
    51  	log              zerolog.Logger
    52  }
    53  
    54  var _ mempool.IncorporatedResultSeals = (*ExecForkSuppressor)(nil)
    55  
    56  // sealSet is a set of seals; internally represented as a map from sealID -> to seal
    57  type sealSet map[flow.Identifier]*flow.IncorporatedResultSeal
    58  
    59  // sealsList is a list of seals
    60  type sealsList []*flow.IncorporatedResultSeal
    61  
    62  func NewExecStateForkSuppressor(seals mempool.IncorporatedResultSeals, onExecFork ExecForkActor, db *badger.DB, log zerolog.Logger) (*ExecForkSuppressor, error) {
    63  	conflictingSeals, err := checkExecutionForkEvidence(db)
    64  	if err != nil {
    65  		return nil, fmt.Errorf("failed to interface with storage: %w", err)
    66  	}
    67  	execForkDetectedFlag := len(conflictingSeals) != 0
    68  	if execForkDetectedFlag {
    69  		onExecFork(conflictingSeals)
    70  	}
    71  
    72  	wrapper := ExecForkSuppressor{
    73  		mutex:            sync.RWMutex{},
    74  		seals:            seals,
    75  		sealsForBlock:    make(map[flow.Identifier]sealSet),
    76  		byHeight:         make(map[uint64]map[flow.Identifier]struct{}),
    77  		execForkDetected: *atomic.NewBool(execForkDetectedFlag),
    78  		onExecFork:       onExecFork,
    79  		db:               db,
    80  		log:              log.With().Str("mempool", "ExecForkSuppressor").Logger(),
    81  	}
    82  
    83  	return &wrapper, nil
    84  }
    85  
    86  // Add adds the given seal to the mempool. Return value indicates whether seal was added to the mempool.
    87  // Internally indexes every added seal by blockID. Expects that underlying mempool never eject items.
    88  // Error returns:
    89  //   - engine.InvalidInputError (sentinel error)
    90  //     In case a seal fails one of the required consistency checks;
    91  func (s *ExecForkSuppressor) Add(newSeal *flow.IncorporatedResultSeal) (bool, error) {
    92  	s.mutex.Lock()
    93  	defer s.mutex.Unlock()
    94  
    95  	if s.execForkDetected.Load() {
    96  		return false, nil
    97  	}
    98  
    99  	if newSeal.Header.Height < s.lowestHeight {
   100  		return false, nil
   101  	}
   102  
   103  	// STEP 1: ensure locally that newSeal's chunks are non zero, which means
   104  	// that the new seal contains start and end state values.
   105  	// This wrapper is a temporary safety layer; we check all conditions that are
   106  	// required for its correct functioning locally, to not delegate safety-critical
   107  	// implementation aspects to external components
   108  	err := s.enforceValidChunks(newSeal)
   109  	if err != nil {
   110  		return false, fmt.Errorf("invalid candidate seal: %w", err)
   111  	}
   112  	blockID := newSeal.Seal.BlockID
   113  
   114  	// This mempool allows adding multiple seals for same blockID even if they have different state transition.
   115  	// When builder logic tries to query such seals we will check whenever we have an execution fork. The main reason for
   116  	// detecting forks at query time(not at adding time) is ability to add extra logic in underlying mempools. For instance
   117  	// we could filter seals comming from underlying mempool by some criteria.
   118  
   119  	// STEP 2: add newSeal to the wrapped mempool
   120  	added, err := s.seals.Add(newSeal) // internally de-duplicates
   121  	if err != nil {
   122  		return added, fmt.Errorf("failed to add seal to wrapped mempool: %w", err)
   123  	}
   124  	if !added { // if underlying mempool did not accept the seal => nothing to do anymore
   125  		return false, nil
   126  	}
   127  
   128  	// STEP 3: add newSeal to secondary index of this wrapper
   129  	// CAUTION: We expect that underlying mempool NEVER ejects seals because it breaks liveness.
   130  	blockSeals, found := s.sealsForBlock[blockID]
   131  	if !found {
   132  		// no other seal for this block was in mempool before => create a set for the seals for this block
   133  		blockSeals = make(sealSet)
   134  		s.sealsForBlock[blockID] = blockSeals
   135  	}
   136  	blockSeals[newSeal.ID()] = newSeal
   137  
   138  	// cache block height to prune additional index by height
   139  	blocksAtHeight, found := s.byHeight[newSeal.Header.Height]
   140  	if !found {
   141  		blocksAtHeight = make(map[flow.Identifier]struct{})
   142  		s.byHeight[newSeal.Header.Height] = blocksAtHeight
   143  	}
   144  	blocksAtHeight[blockID] = struct{}{}
   145  
   146  	return true, nil
   147  }
   148  
   149  // All returns all the IncorporatedResultSeals in the mempool.
   150  // Note: This call might crash if the block of the seal has multiple seals in mempool for conflicting
   151  // incorporated results.
   152  func (s *ExecForkSuppressor) All() []*flow.IncorporatedResultSeal {
   153  	s.mutex.RLock()
   154  	seals := s.seals.All()
   155  	s.mutex.RUnlock()
   156  
   157  	// index seals retrieved from underlying mepool by blockID to check
   158  	// for conflicting seals
   159  	sealsByBlockID := make(map[flow.Identifier]sealsList, 0)
   160  	for _, seal := range seals {
   161  		sealsPerBlock := sealsByBlockID[seal.Seal.BlockID]
   162  		sealsByBlockID[seal.Seal.BlockID] = append(sealsPerBlock, seal)
   163  	}
   164  
   165  	// check for conflicting seals
   166  	return s.filterConflictingSeals(sealsByBlockID)
   167  }
   168  
   169  // ByID returns an IncorporatedResultSeal by its ID.
   170  // The IncorporatedResultSeal's ID is the same as IncorporatedResult's ID,
   171  // so this call essentially is to find the seal for the incorporated result in the mempool.
   172  // Note: This call might crash if the block of the seal has multiple seals in mempool for conflicting
   173  // incorporated results. Usually the builder will call this method to find a seal for an incorporated
   174  // result, so the builder might crash if multiple conflicting seals exist.
   175  func (s *ExecForkSuppressor) ByID(identifier flow.Identifier) (*flow.IncorporatedResultSeal, bool) {
   176  	s.mutex.RLock()
   177  	seal, found := s.seals.ByID(identifier)
   178  	// if we haven't found seal in underlying storage - exit early
   179  	if !found {
   180  		s.mutex.RUnlock()
   181  		return seal, found
   182  	}
   183  	sealsForBlock := s.sealsForBlock[seal.Seal.BlockID]
   184  	// if there are no other seals for this block previously seen - then no possible execution forks
   185  	if len(sealsForBlock) == 1 {
   186  		s.mutex.RUnlock()
   187  		return seal, true
   188  	}
   189  	// convert map into list
   190  	var sealsPerBlock sealsList
   191  	for _, otherSeal := range sealsForBlock {
   192  		sealsPerBlock = append(sealsPerBlock, otherSeal)
   193  	}
   194  	s.mutex.RUnlock()
   195  
   196  	// check for conflicting seals
   197  	seals := s.filterConflictingSeals(map[flow.Identifier]sealsList{seal.Seal.BlockID: sealsPerBlock})
   198  	if len(seals) == 0 {
   199  		return nil, false
   200  	}
   201  	return seals[0], true
   202  }
   203  
   204  // Remove removes the IncorporatedResultSeal with id from the mempool
   205  func (s *ExecForkSuppressor) Remove(id flow.Identifier) bool {
   206  	s.mutex.Lock()
   207  	defer s.mutex.Unlock()
   208  
   209  	seal, found := s.seals.ByID(id)
   210  	if found {
   211  		s.seals.Remove(id)
   212  		set, found := s.sealsForBlock[seal.Seal.BlockID]
   213  		if !found {
   214  			// In the current implementation, this cannot happen, as every entity in the mempool is also contained in sealsForBlock.
   215  			// we nevertheless perform this sanity check here, to catch future inconsistent code modifications
   216  			s.log.Fatal().Msg("inconsistent state detected: seal not in secondary index")
   217  		}
   218  		if len(set) > 1 {
   219  			delete(set, id)
   220  		} else {
   221  			delete(s.sealsForBlock, seal.Seal.BlockID)
   222  		}
   223  	}
   224  	return found
   225  }
   226  
   227  // Size returns the number of items in the mempool
   228  func (s *ExecForkSuppressor) Size() uint {
   229  	s.mutex.RLock()
   230  	defer s.mutex.RUnlock()
   231  	return s.seals.Size()
   232  }
   233  
   234  // Limit returns the size limit of the mempool
   235  func (s *ExecForkSuppressor) Limit() uint {
   236  	s.mutex.RLock()
   237  	defer s.mutex.RUnlock()
   238  	return s.seals.Limit()
   239  }
   240  
   241  // Clear removes all entities from the pool.
   242  // The wrapper clears the internal state as well as its local (additional) state.
   243  func (s *ExecForkSuppressor) Clear() {
   244  	s.mutex.Lock()
   245  	defer s.mutex.Unlock()
   246  	s.sealsForBlock = make(map[flow.Identifier]sealSet)
   247  	s.seals.Clear()
   248  }
   249  
   250  // PruneUpToHeight remove all seals for blocks whose height is strictly
   251  // smaller that height. Note: seals for blocks at height are retained.
   252  func (s *ExecForkSuppressor) PruneUpToHeight(height uint64) error {
   253  	err := s.seals.PruneUpToHeight(height)
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	s.mutex.Lock()
   259  	defer s.mutex.Unlock()
   260  
   261  	if len(s.sealsForBlock) == 0 {
   262  		s.lowestHeight = height
   263  		return nil
   264  	}
   265  
   266  	// Optimization: if there are less height in the index than the height range to prune,
   267  	// range to prune, then just go through each seal.
   268  	// Otherwise, go through each height to prune.
   269  	if uint64(len(s.byHeight)) < height-s.lowestHeight {
   270  		for h := range s.byHeight {
   271  			if h < height {
   272  				s.removeByHeight(h)
   273  			}
   274  		}
   275  	} else {
   276  		for h := s.lowestHeight; h < height; h++ {
   277  			s.removeByHeight(h)
   278  		}
   279  	}
   280  
   281  	return nil
   282  }
   283  
   284  func (s *ExecForkSuppressor) removeByHeight(height uint64) {
   285  	for blockID := range s.byHeight[height] {
   286  		delete(s.sealsForBlock, blockID)
   287  	}
   288  	delete(s.byHeight, height)
   289  }
   290  
   291  // enforceValidChunks checks that seal has valid non-zero number of chunks.
   292  // In case a seal fails the check, a detailed error message is logged and an
   293  // engine.InvalidInputError (sentinel error) is returned.
   294  func (s *ExecForkSuppressor) enforceValidChunks(irSeal *flow.IncorporatedResultSeal) error {
   295  	result := irSeal.IncorporatedResult.Result
   296  
   297  	if !result.ValidateChunksLength() {
   298  		scjson, errjson := json.Marshal(irSeal)
   299  		if errjson != nil {
   300  			return errjson
   301  		}
   302  		s.log.Error().
   303  			Str("seal", string(scjson)).
   304  			Msg("seal's execution result has no chunks")
   305  		return engine.NewInvalidInputErrorf("seal's execution result has no chunks: %x", result.ID())
   306  	}
   307  	return nil
   308  }
   309  
   310  // enforceConsistentStateTransitions checks whether the execution results in the seals
   311  // have matching state transitions. If a fork in the execution state is detected:
   312  //   - wrapped mempool is cleared
   313  //   - internal execForkDetected flag is ste to true
   314  //   - the new value of execForkDetected is persisted to data base
   315  //
   316  // and executionForkErr (sentinel error) is returned
   317  // The function assumes the execution results in the seals have a non-zero number of chunks.
   318  func hasConsistentStateTransitions(irSeal, irSeal2 *flow.IncorporatedResultSeal) bool {
   319  	if irSeal.IncorporatedResult.Result.ID() == irSeal2.IncorporatedResult.Result.ID() {
   320  		// happy case: candidate seals are for the same result
   321  		return true
   322  	}
   323  	// the results for the seals have different IDs (!)
   324  	// => check whether initial and final state match in both seals
   325  
   326  	// unsafe: we assume validity of chunks has been checked before
   327  	irSeal1InitialState, _ := irSeal.IncorporatedResult.Result.InitialStateCommit()
   328  	irSeal1FinalState, _ := irSeal.IncorporatedResult.Result.FinalStateCommitment()
   329  	irSeal2InitialState, _ := irSeal2.IncorporatedResult.Result.InitialStateCommit()
   330  	irSeal2FinalState, _ := irSeal2.IncorporatedResult.Result.FinalStateCommitment()
   331  
   332  	if irSeal1InitialState != irSeal2InitialState || irSeal1FinalState != irSeal2FinalState {
   333  		log.Error().Msg("inconsistent seals for the same block")
   334  		return false
   335  	}
   336  	log.Warn().Msg("seals with different ID but consistent state transition")
   337  	return true
   338  }
   339  
   340  // checkExecutionForkDetected checks the database whether evidence
   341  // about an execution fork is stored. Returns the stored evidence.
   342  func checkExecutionForkEvidence(db *badger.DB) ([]*flow.IncorporatedResultSeal, error) {
   343  	var conflictingSeals []*flow.IncorporatedResultSeal
   344  	err := db.View(func(tx *badger.Txn) error {
   345  		err := operation.RetrieveExecutionForkEvidence(&conflictingSeals)(tx)
   346  		if errors.Is(err, storage.ErrNotFound) {
   347  			return nil // no evidence in data base; conflictingSeals is still nil slice
   348  		}
   349  		if err != nil {
   350  			return fmt.Errorf("failed to load evidence whether or not an execution fork occured: %w", err)
   351  		}
   352  		return nil
   353  	})
   354  	return conflictingSeals, err
   355  }
   356  
   357  // storeExecutionForkEvidence stores the provided seals in the database
   358  // as evidence for an execution fork.
   359  func storeExecutionForkEvidence(conflictingSeals []*flow.IncorporatedResultSeal, db *badger.DB) error {
   360  	err := operation.RetryOnConflict(db.Update, func(tx *badger.Txn) error {
   361  		err := operation.InsertExecutionForkEvidence(conflictingSeals)(tx)
   362  		if errors.Is(err, storage.ErrAlreadyExists) {
   363  			// some evidence about execution fork already stored;
   364  			// we only keep the first evidence => noting more to do
   365  			return nil
   366  		}
   367  		if err != nil {
   368  			return fmt.Errorf("failed to store evidence about execution fork: %w", err)
   369  		}
   370  		return nil
   371  	})
   372  	return err
   373  }
   374  
   375  // filterConflictingSeals performs filtering of provided seals by checking if there are conflicting seals for same block.
   376  // For every block we check if first seal has same state transitions as others. Multiple seals for same block are allowed
   377  // but their state transitions should be the same. Upon detecting seal with inconsistent state transition we will clear our mempool,
   378  // stop accepting new seals and querying old seals and store execution fork evidence into DB. Creator of mempool will be notified
   379  // by callback.
   380  func (s *ExecForkSuppressor) filterConflictingSeals(sealsByBlockID map[flow.Identifier]sealsList) sealsList {
   381  	var result sealsList
   382  	for _, sealsInBlock := range sealsByBlockID {
   383  		if len(sealsInBlock) > 1 {
   384  			// enforce that newSeal's state transition does not conflict with other stored seals for the same block
   385  			// already other seal for this block in mempool => compare consistency of results' state transitions
   386  			var conflictingSeals sealsList
   387  			candidateSeal := sealsInBlock[0]
   388  			for _, otherSeal := range sealsInBlock[1:] {
   389  				if !hasConsistentStateTransitions(candidateSeal, otherSeal) {
   390  					conflictingSeals = append(conflictingSeals, otherSeal)
   391  				}
   392  			}
   393  			// check if inconsistent state transition detected
   394  			if len(conflictingSeals) > 0 {
   395  				s.execForkDetected.Store(true)
   396  				s.Clear()
   397  				conflictingSeals = append(sealsList{candidateSeal}, conflictingSeals...)
   398  				err := storeExecutionForkEvidence(conflictingSeals, s.db)
   399  				if err != nil {
   400  					panic("failed to store execution fork evidence")
   401  				}
   402  				s.onExecFork(conflictingSeals)
   403  				return nil
   404  			}
   405  		}
   406  		result = append(result, sealsInBlock...)
   407  	}
   408  	return result
   409  }