github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/execution/ingestion/block_queue/queue.go (about)

     1  package block_queue
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  
     7  	"github.com/rs/zerolog"
     8  
     9  	"github.com/onflow/flow-go/model/flow"
    10  	"github.com/onflow/flow-go/module/mempool/entity"
    11  )
    12  
    13  var ErrMissingParent = fmt.Errorf("missing parent block")
    14  
    15  // BlockQueue keeps track of state of blocks and determines which blocks are executable
    16  // A block becomes executable when all the following conditions are met:
    17  // 1. the block has been validated by consensus algorithm
    18  // 2. the block's parent has been executed
    19  // 3. all the collections included in the block have been received
    20  type BlockQueue struct {
    21  	sync.Mutex
    22  	log zerolog.Logger
    23  
    24  	// if a block still exists in this map, it means the block has not been executed.
    25  	// it could either be one of the following cases:
    26  	// 1) block is not executed due to some of its collection is missing
    27  	// 2) block is not executed due to its parent block has not been executed
    28  	// 3) block is ready to execute, but the execution has not been finished yet.
    29  	// some consistency checks:
    30  	// 1) since an executed block must have been removed from this map, if a block's
    31  	// parent block has been executed, then its parent block must have been removed
    32  	// from this map
    33  	// 2) if a block's parent block has not been executed, then its parent block must still
    34  	// exist in this map
    35  	blocks map[flow.Identifier]*entity.ExecutableBlock
    36  
    37  	// a collection could be included in multiple blocks,
    38  	// when a missing block is received, it might trigger multiple blocks to be executable, which
    39  	// can be looked up by the map
    40  	// when a block is executed, its collections should be removed from this map unless a collection
    41  	// is still referenced by other blocks, which will eventually be removed when those blocks are
    42  	// executed.
    43  	collections map[flow.Identifier]*collectionInfo
    44  
    45  	// blockIDsByHeight is used to find next executable block.
    46  	// when a block is executed, the next executable block must be a block with height = current block height + 1
    47  	// the following map allows us to find the next executable block by height and their parent block ID
    48  	blockIDsByHeight map[uint64]map[flow.Identifier]*entity.ExecutableBlock
    49  }
    50  
    51  type MissingCollection struct {
    52  	BlockID   flow.Identifier
    53  	Height    uint64
    54  	Guarantee *flow.CollectionGuarantee
    55  }
    56  
    57  func (m *MissingCollection) ID() flow.Identifier {
    58  	return m.Guarantee.ID()
    59  }
    60  
    61  // collectionInfo is an internal struct used to keep track of the state of a collection,
    62  // and the blocks that include the collection
    63  type collectionInfo struct {
    64  	Collection *entity.CompleteCollection
    65  	IncludedIn map[flow.Identifier]*entity.ExecutableBlock
    66  }
    67  
    68  func NewBlockQueue(logger zerolog.Logger) *BlockQueue {
    69  	log := logger.With().Str("module", "block_queue").Logger()
    70  
    71  	return &BlockQueue{
    72  		log:              log,
    73  		blocks:           make(map[flow.Identifier]*entity.ExecutableBlock),
    74  		collections:      make(map[flow.Identifier]*collectionInfo),
    75  		blockIDsByHeight: make(map[uint64]map[flow.Identifier]*entity.ExecutableBlock),
    76  	}
    77  }
    78  
    79  // HandleBlock is called when a new block is received, the parentFinalState indicates
    80  // whether its parent block has been executed.
    81  // Caller must ensure:
    82  // 1. blocks are passsed in order, i.e. parent block is passed in before its child block
    83  // 2. if a block's parent is not executed, then the parent block must be passed in first
    84  // 3. if a block's parent is executed, then the parent's finalState must be passed in
    85  // It returns (nil, nil, nil) if this block is a duplication
    86  func (q *BlockQueue) HandleBlock(block *flow.Block, parentFinalState *flow.StateCommitment) (
    87  	[]*MissingCollection, // missing collections
    88  	[]*entity.ExecutableBlock, // blocks ready to execute
    89  	error, // exceptions
    90  ) {
    91  	q.Lock()
    92  	defer q.Unlock()
    93  
    94  	// check if the block already exists
    95  	blockID := block.ID()
    96  	executable, ok := q.blocks[blockID]
    97  
    98  	q.log.Debug().
    99  		Str("blockID", blockID.String()).
   100  		Uint64("height", block.Header.Height).
   101  		Bool("parent executed", parentFinalState != nil).
   102  		Msg("handle block")
   103  
   104  	if ok {
   105  		// handle the case where the block has seen before
   106  		return q.handleKnownBlock(executable, parentFinalState)
   107  	}
   108  
   109  	// handling a new block
   110  
   111  	_, parentExists := q.blocks[block.Header.ParentID]
   112  	// if parentFinalState is not provided, then its parent block must exists in the queue
   113  	// otherwise it's an exception
   114  	if parentFinalState == nil {
   115  		if !parentExists {
   116  			return nil, nil,
   117  				fmt.Errorf("block %s has no parent commitment, but its parent block %s does not exist in the queue: %w",
   118  					blockID, block.Header.ParentID, ErrMissingParent)
   119  		}
   120  	} else {
   121  		if parentExists {
   122  			// this is an edge case where A <- B, and B is received with A's final state, however,
   123  			// A is not executed yet.
   124  			// the reason this could happen is that there is a race condition in `OnBlockExecuted` and
   125  			// `HandleBlock`, when A's execution result (which contains the final state) has been
   126  			// saved to database, and B is received before `blockQueue.OnBlockExecuted(A)` is called, so
   127  			// `blockQueue.HandleBlock(B, A's final state)` will be called, which run into this case.
   128  			// In this case, if we consider A is executed, then return B as executables, and later
   129  			// when `OnBlockExecuted(A)` is called, it will return B as executables again, which
   130  			// will cause B to be executed twice.
   131  			// In order to prevent B to be executed twice, we will simply ignore A's final state,
   132  			// as if its parent has not been executed yet, then B is not executable. And when `OnBlockExecuted(A)`
   133  			// is called, it will return B as executables, so that both A and B will be executed only once.
   134  			// See test case: TestHandleBlockChildCalledBeforeOnBlockExecutedParent
   135  			q.log.Warn().
   136  				Str("blockID", blockID.String()).
   137  				Uint64("height", block.Header.Height).
   138  				Msgf("edge case: receiving block with parent commitment, but its parent block %s still exists",
   139  					block.Header.ParentID)
   140  
   141  			parentFinalState = nil
   142  		}
   143  	}
   144  
   145  	executable = &entity.ExecutableBlock{
   146  		Block:      block,
   147  		StartState: parentFinalState,
   148  	}
   149  
   150  	// add block to blocks
   151  	q.blocks[blockID] = executable
   152  
   153  	// update collection
   154  	colls := make(map[flow.Identifier]*entity.CompleteCollection, len(block.Payload.Guarantees))
   155  	executable.CompleteCollections = colls
   156  
   157  	// find missing collections and update collection index
   158  	missingCollections := make([]*MissingCollection, 0, len(block.Payload.Guarantees))
   159  
   160  	for _, guarantee := range block.Payload.Guarantees {
   161  		colID := guarantee.ID()
   162  		colInfo, ok := q.collections[colID]
   163  		if ok {
   164  			// some other block also includes this collection
   165  			colInfo.IncludedIn[blockID] = executable
   166  			colls[colID] = colInfo.Collection
   167  		} else {
   168  			col := &entity.CompleteCollection{
   169  				Guarantee: guarantee,
   170  			}
   171  			colls[colID] = col
   172  
   173  			// add new collection to collections
   174  			q.collections[colID] = &collectionInfo{
   175  				Collection: col,
   176  				IncludedIn: map[flow.Identifier]*entity.ExecutableBlock{
   177  					blockID: executable,
   178  				},
   179  			}
   180  
   181  			missingCollections = append(missingCollections, missingCollectionForBlock(executable, guarantee))
   182  		}
   183  	}
   184  
   185  	// index height
   186  	blocksAtSameHeight, ok := q.blockIDsByHeight[block.Header.Height]
   187  	if !ok {
   188  		blocksAtSameHeight = make(map[flow.Identifier]*entity.ExecutableBlock)
   189  		q.blockIDsByHeight[block.Header.Height] = blocksAtSameHeight
   190  	}
   191  	blocksAtSameHeight[blockID] = executable
   192  
   193  	// check if the block is executable
   194  	var executables []*entity.ExecutableBlock
   195  	if executable.IsComplete() {
   196  		// executables might contain other siblings, but won't contain "executable",
   197  		// which is the block itself, that's because executables are created
   198  		// from OnBlockExecuted(
   199  		executables = []*entity.ExecutableBlock{executable}
   200  	}
   201  
   202  	return missingCollections, executables, nil
   203  }
   204  
   205  // HandleCollection is called when a new collection is received
   206  // It returns a list of executable blocks that contains the collection
   207  func (q *BlockQueue) HandleCollection(collection *flow.Collection) ([]*entity.ExecutableBlock, error) {
   208  	q.Lock()
   209  	defer q.Unlock()
   210  	// when a collection is received, we find the blocks the collection is included in,
   211  	// and check if the blocks become executable.
   212  	// Note a collection could be included in multiple blocks, so receiving a collection
   213  	// might trigger multiple blocks to be executable.
   214  
   215  	// check if the collection is for any block in the queue
   216  	colID := collection.ID()
   217  	colInfo, ok := q.collections[colID]
   218  	if !ok {
   219  		// no block in the queue includes this collection
   220  		return nil, nil
   221  	}
   222  
   223  	if colInfo.Collection.IsCompleted() {
   224  		// the collection is already received, no action needed because an action must
   225  		// have been returned when the collection is first received.
   226  		return nil, nil
   227  	}
   228  
   229  	// update collection
   230  	colInfo.Collection.Transactions = collection.Transactions
   231  
   232  	// check if any block, which includes this collection, became executable
   233  	executables := make([]*entity.ExecutableBlock, 0, len(colInfo.IncludedIn))
   234  	for _, block := range colInfo.IncludedIn {
   235  		if !block.IsComplete() {
   236  			continue
   237  		}
   238  		executables = append(executables, block)
   239  	}
   240  
   241  	if len(executables) == 0 {
   242  		return nil, nil
   243  	}
   244  
   245  	return executables, nil
   246  }
   247  
   248  // OnBlockExecuted is called when a block is executed
   249  // It returns a list of executable blocks (usually its child blocks)
   250  // The caller has to ensure OnBlockExecuted is not called in a wrong order, such as
   251  // OnBlockExecuted(childBlock) being called before OnBlockExecuted(parentBlock).
   252  func (q *BlockQueue) OnBlockExecuted(
   253  	blockID flow.Identifier,
   254  	commit flow.StateCommitment,
   255  ) ([]*entity.ExecutableBlock, error) {
   256  	q.Lock()
   257  	defer q.Unlock()
   258  
   259  	q.log.Debug().
   260  		Str("blockID", blockID.String()).
   261  		Hex("commit", commit[:]).
   262  		Msg("block executed")
   263  
   264  	return q.onBlockExecuted(blockID, commit)
   265  }
   266  
   267  func (q *BlockQueue) handleKnownBlock(executable *entity.ExecutableBlock, parentFinalState *flow.StateCommitment) (
   268  	[]*MissingCollection, // missing collections
   269  	[]*entity.ExecutableBlock, // blocks ready to execute
   270  	error, // exceptions
   271  ) {
   272  	// we have already received this block, and its parent still has not been executed yet
   273  	if executable.StartState == nil && parentFinalState == nil {
   274  		return nil, nil, nil
   275  	}
   276  
   277  	// this is an edge case where parentFinalState is provided, and its parent block exists
   278  	// in the queue but has not been marked as executed yet (OnBlockExecuted(parent) is not called),
   279  	// in this case, we will internally call OnBlockExecuted(parentBlockID, parentFinalState).
   280  	// there is no need to create the executable block again, since it's already created.
   281  	if executable.StartState == nil && parentFinalState != nil {
   282  		q.log.Warn().
   283  			Str("blockID", executable.ID().String()).
   284  			Uint64("height", executable.Block.Header.Height).
   285  			Hex("parentID", executable.Block.Header.ParentID[:]).
   286  			Msg("edge case: receiving block with no parent commitment, but its parent block actually has been executed")
   287  
   288  		executables, err := q.onBlockExecuted(executable.Block.Header.ParentID, *parentFinalState)
   289  		if err != nil {
   290  			return nil, nil, fmt.Errorf("receiving block %v with parent commitment %v, but parent block %v already exists with no commitment, fail to call mark parent as executed: %w",
   291  				executable.ID(), *parentFinalState, executable.Block.Header.ParentID, err)
   292  		}
   293  
   294  		// we already have this block, its collection must have been fetched, so we only return the
   295  		// executables from marking its parent as executed.
   296  		return nil, executables, nil
   297  	}
   298  
   299  	// this means the caller think it's parent has not been executed, but the queue's internal state
   300  	// shows the parent has been executed, then it's probably a race condition where the call to
   301  	// inform the parent block has been executed arrives earlier than this call, which is an edge case
   302  	// and we can simply ignore this call.
   303  	if executable.StartState != nil && parentFinalState == nil {
   304  		q.log.Warn().
   305  			Str("blockID", executable.ID().String()).
   306  			Uint64("height", executable.Block.Header.Height).
   307  			Hex("parentID", executable.Block.Header.ParentID[:]).
   308  			Msg("edge case: receiving block with no parent commitment, but its parent block actually has been executed")
   309  		return nil, nil, nil
   310  	}
   311  
   312  	// this is an exception that should not happen
   313  	if *executable.StartState != *parentFinalState {
   314  		return nil, nil,
   315  			fmt.Errorf("block %s has already been executed with a different parent final state, %v != %v",
   316  				executable.ID(), *executable.StartState, parentFinalState)
   317  	}
   318  
   319  	q.log.Warn().
   320  		Str("blockID", executable.ID().String()).
   321  		Uint64("height", executable.Block.Header.Height).
   322  		Msg("edge case: OnBlockExecuted is called with the same arguments again")
   323  	return nil, nil, nil
   324  }
   325  
   326  func (q *BlockQueue) onBlockExecuted(
   327  	blockID flow.Identifier,
   328  	commit flow.StateCommitment,
   329  ) ([]*entity.ExecutableBlock, error) {
   330  	// when a block is executed, the child block might become executable
   331  	// we also remove it from all the indexes
   332  
   333  	// remove block
   334  	block, ok := q.blocks[blockID]
   335  	if !ok {
   336  		return nil, nil
   337  	}
   338  
   339  	// sanity check
   340  	// if a block exists in the queue and is executed, then its parent block
   341  	// must not exist in the queue, otherwise the state is inconsistent
   342  	_, parentExists := q.blocks[block.Block.Header.ParentID]
   343  	if parentExists {
   344  		return nil, fmt.Errorf("parent block %s of block %s is in the queue",
   345  			block.Block.Header.ParentID, blockID)
   346  	}
   347  
   348  	delete(q.blocks, blockID)
   349  
   350  	// remove height index
   351  	height := block.Block.Header.Height
   352  	delete(q.blockIDsByHeight[height], blockID)
   353  	if len(q.blockIDsByHeight[height]) == 0 {
   354  		delete(q.blockIDsByHeight, height)
   355  	}
   356  
   357  	// remove colections if no other blocks include it
   358  	for colID := range block.CompleteCollections {
   359  		colInfo, ok := q.collections[colID]
   360  		if !ok {
   361  			return nil, fmt.Errorf("collection %s not found", colID)
   362  		}
   363  
   364  		delete(colInfo.IncludedIn, blockID)
   365  		if len(colInfo.IncludedIn) == 0 {
   366  			// no other blocks includes this collection,
   367  			// so this collection can be removed from the index
   368  			delete(q.collections, colID)
   369  		}
   370  	}
   371  
   372  	return q.checkIfChildBlockBecomeExecutable(block, commit)
   373  }
   374  
   375  func (q *BlockQueue) checkIfChildBlockBecomeExecutable(
   376  	block *entity.ExecutableBlock,
   377  	commit flow.StateCommitment,
   378  ) ([]*entity.ExecutableBlock, error) {
   379  	childHeight := block.Block.Header.Height + 1
   380  	blocksAtNextHeight, ok := q.blockIDsByHeight[childHeight]
   381  	if !ok {
   382  		// no block at next height
   383  		return nil, nil
   384  	}
   385  
   386  	// find children and update their start state
   387  	children := make([]*entity.ExecutableBlock, 0, len(blocksAtNextHeight))
   388  	for _, childBlock := range blocksAtNextHeight {
   389  		// a child block at the next height must have the same parent ID
   390  		// as the current block
   391  		isChild := childBlock.Block.Header.ParentID == block.ID()
   392  		if !isChild {
   393  			continue
   394  		}
   395  
   396  		// update child block's start state with current block's end state
   397  		childBlock.StartState = &commit
   398  		children = append(children, childBlock)
   399  	}
   400  
   401  	if len(children) == 0 {
   402  		return nil, nil
   403  	}
   404  
   405  	// check if children are executable
   406  	executables := make([]*entity.ExecutableBlock, 0, len(children))
   407  	for _, child := range children {
   408  		if child.IsComplete() {
   409  			executables = append(executables, child)
   410  		}
   411  	}
   412  
   413  	return executables, nil
   414  }
   415  
   416  // GetMissingCollections returns the missing collections and the start state for the given block
   417  // Useful for debugging what is missing for the next unexecuted block to become executable.
   418  // It returns an error if the block is not found
   419  func (q *BlockQueue) GetMissingCollections(blockID flow.Identifier) (
   420  	[]*MissingCollection,
   421  	*flow.StateCommitment,
   422  	error,
   423  ) {
   424  	q.Lock()
   425  	defer q.Unlock()
   426  	block, ok := q.blocks[blockID]
   427  	if !ok {
   428  		return nil, nil, fmt.Errorf("block %s not found", blockID)
   429  	}
   430  
   431  	missingCollections := make([]*MissingCollection, 0, len(block.Block.Payload.Guarantees))
   432  	for _, col := range block.CompleteCollections {
   433  		// check if the collection is already received
   434  		if col.IsCompleted() {
   435  			continue
   436  		}
   437  		missingCollections = append(missingCollections, missingCollectionForBlock(block, col.Guarantee))
   438  	}
   439  
   440  	return missingCollections, block.StartState, nil
   441  }
   442  
   443  func missingCollectionForBlock(block *entity.ExecutableBlock, guarantee *flow.CollectionGuarantee) *MissingCollection {
   444  	return &MissingCollection{
   445  		BlockID:   block.ID(),
   446  		Height:    block.Block.Header.Height,
   447  		Guarantee: guarantee,
   448  	}
   449  }