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 }