github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/verification/assigner/engine_test.go (about) 1 package assigner_test 2 3 import ( 4 "sync" 5 "testing" 6 "time" 7 8 "github.com/rs/zerolog" 9 "github.com/stretchr/testify/mock" 10 "github.com/stretchr/testify/require" 11 12 "github.com/onflow/flow-go/engine/verification/assigner" 13 vertestutils "github.com/onflow/flow-go/engine/verification/utils/unittest" 14 "github.com/onflow/flow-go/model/chunks" 15 "github.com/onflow/flow-go/model/flow" 16 module "github.com/onflow/flow-go/module/mock" 17 "github.com/onflow/flow-go/module/trace" 18 protocol "github.com/onflow/flow-go/state/protocol/mock" 19 storage "github.com/onflow/flow-go/storage/mock" 20 "github.com/onflow/flow-go/utils/unittest" 21 ) 22 23 // AssignerEngineTestSuite encapsulates data structures for running unittests on assigner engine. 24 type AssignerEngineTestSuite struct { 25 // modules 26 me *module.Local 27 state *protocol.State 28 snapshot *protocol.Snapshot 29 metrics *module.VerificationMetrics 30 tracer *trace.NoopTracer 31 assigner *module.ChunkAssigner 32 chunksQueue *storage.ChunksQueue 33 newChunkListener *module.NewJobListener 34 notifier *module.ProcessingNotifier 35 36 // identities 37 verIdentity *flow.Identity // verification node 38 } 39 40 // mockChunkAssigner mocks the chunk assigner of this test suite to assign the chunks based on the input assignment. 41 // It returns number of chunks assigned to verification node of this test suite. 42 func (s *AssignerEngineTestSuite) mockChunkAssigner(result *flow.IncorporatedResult, assignment *chunks.Assignment) int { 43 s.assigner.On("Assign", result.Result, result.IncorporatedBlockID).Return(assignment, nil).Once() 44 assignedChunks := assignment.ByNodeID(s.myID()) 45 s.metrics.On("OnChunksAssignmentDoneAtAssigner", len(assignedChunks)).Return().Once() 46 return len(assignedChunks) 47 } 48 49 // mockStateAtBlockID is a test helper that mocks the protocol state of test suite at the given block id. This is the 50 // underlying protocol state of the verification node of the test suite. 51 func (s *AssignerEngineTestSuite) mockStateAtBlockID(blockID flow.Identifier) { 52 s.state.On("AtBlockID", blockID).Return(s.snapshot) 53 s.snapshot.On("Identity", s.verIdentity.NodeID).Return(s.verIdentity, nil) 54 } 55 56 // myID is a test helper that returns identifier of verification identity. 57 func (s *AssignerEngineTestSuite) myID() flow.Identifier { 58 return s.verIdentity.NodeID 59 } 60 61 func WithIdentity(identity *flow.Identity) func(*AssignerEngineTestSuite) { 62 return func(testSuite *AssignerEngineTestSuite) { 63 testSuite.verIdentity = identity 64 } 65 } 66 67 // SetupTest initiates the test setups prior to each test. 68 func SetupTest(options ...func(suite *AssignerEngineTestSuite)) *AssignerEngineTestSuite { 69 s := &AssignerEngineTestSuite{ 70 me: &module.Local{}, 71 state: &protocol.State{}, 72 snapshot: &protocol.Snapshot{}, 73 metrics: &module.VerificationMetrics{}, 74 tracer: trace.NewNoopTracer(), 75 assigner: &module.ChunkAssigner{}, 76 chunksQueue: &storage.ChunksQueue{}, 77 newChunkListener: &module.NewJobListener{}, 78 verIdentity: unittest.IdentityFixture(unittest.WithRole(flow.RoleVerification)), 79 notifier: &module.ProcessingNotifier{}, 80 } 81 82 for _, apply := range options { 83 apply(s) 84 } 85 return s 86 } 87 88 // createContainerBlock creates and returns a block that contains an execution receipt, with its corresponding chunks assignment based 89 // on the input options. 90 func createContainerBlock(options ...func(result *flow.ExecutionResult, assignments *chunks.Assignment)) (*flow.Block, *chunks.Assignment) { 91 result, assignment := vertestutils.CreateExecutionResult(unittest.IdentifierFixture(), options...) 92 receipt := &flow.ExecutionReceipt{ 93 ExecutorID: unittest.IdentifierFixture(), 94 ExecutionResult: *result, 95 } 96 // container block 97 header := unittest.BlockHeaderFixture() 98 block := &flow.Block{ 99 Header: header, 100 Payload: &flow.Payload{ 101 Receipts: []*flow.ExecutionReceiptMeta{receipt.Meta()}, 102 Results: []*flow.ExecutionResult{&receipt.ExecutionResult}, 103 }, 104 } 105 return block, assignment 106 } 107 108 // NewAssignerEngine returns an assigner engine for testing. 109 func NewAssignerEngine(s *AssignerEngineTestSuite) *assigner.Engine { 110 111 e := assigner.New(zerolog.Logger{}, 112 s.metrics, 113 s.tracer, 114 s.me, 115 s.state, 116 s.assigner, 117 s.chunksQueue, 118 s.newChunkListener, 0) 119 120 e.WithBlockConsumerNotifier(s.notifier) 121 122 // mocks identity of the verification node 123 s.me.On("NodeID").Return(s.verIdentity.NodeID) 124 125 return e 126 } 127 128 // TestAssignerEngine runs all subtests in parallel. 129 func TestAssignerEngine(t *testing.T) { 130 t.Parallel() 131 t.Run("new block happy path", func(t *testing.T) { 132 newBlockHappyPath(t) 133 }) 134 t.Run("new block invalid identity", func(t *testing.T) { 135 newBlockVerifierNotAuthorized(t) 136 }) 137 t.Run("new block zero chunk", func(t *testing.T) { 138 newBlockNoChunk(t) 139 }) 140 t.Run("new block no assigned chunk", func(t *testing.T) { 141 newBlockNoAssignedChunk(t) 142 }) 143 t.Run("new block multiple assignments", func(t *testing.T) { 144 newBlockMultipleAssignment(t) 145 }) 146 t.Run("chunk queue unhappy path duplicate", func(t *testing.T) { 147 chunkQueueUnhappyPathDuplicate(t) 148 }) 149 } 150 151 // newBlockHappyPath evaluates that passing a new finalized block to assigner engine that contains 152 // a receipt with one assigned chunk, results in the assigner engine passing the assigned chunk to the 153 // chunks queue and notifying the job listener of the assigned chunks. 154 func newBlockHappyPath(t *testing.T) { 155 s := SetupTest() 156 e := NewAssignerEngine(s) 157 158 // creates a container block, with a single receipt, that contains 159 // one assigned chunk to verification node. 160 containerBlock, assignment := createContainerBlock( 161 vertestutils.WithChunks( 162 vertestutils.WithAssignee(s.myID()))) 163 result := containerBlock.Payload.Results[0] 164 s.mockStateAtBlockID(result.BlockID) 165 chunksNum := s.mockChunkAssigner(flow.NewIncorporatedResult(containerBlock.ID(), result), assignment) 166 require.Equal(t, chunksNum, 1) // one chunk should be assigned 167 168 // mocks processing assigned chunks 169 // each assigned chunk should be stored in the chunks queue and new chunk listener should be 170 // invoked for it. 171 // Also, once all receipts of the block processed, engine should notify the block consumer once, that 172 // it is done with processing this chunk. 173 chunksQueueWG := mockChunksQueueForAssignment(t, s.verIdentity.NodeID, s.chunksQueue, result.ID(), assignment, true, nil) 174 s.newChunkListener.On("Check").Return().Times(chunksNum) 175 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 176 s.metrics.On("OnAssignedChunkProcessedAtAssigner").Return().Once() 177 178 // sends containerBlock containing receipt to assigner engine 179 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 180 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 181 e.ProcessFinalizedBlock(containerBlock) 182 183 unittest.RequireReturnsBefore(t, chunksQueueWG.Wait, 10*time.Millisecond, "could not receive chunk locators") 184 185 mock.AssertExpectationsForObjects(t, 186 s.metrics, 187 s.assigner, 188 s.newChunkListener, 189 s.notifier) 190 } 191 192 // newBlockVerifierNotAuthorized evaluates that when verification node is not authorized to participate at reference block, it includes next cases: 193 // - verification node is joining 194 // - verification node is leaving 195 // - verification node has zero initial weight. 196 // It drops the corresponding execution receipts for that block without performing any chunk assignment. 197 // It also evaluates that the chunks queue is never called on any chunks of that receipt's result. 198 func newBlockVerifierNotAuthorized(t *testing.T) { 199 200 assertIdentityAtReferenceBlock := func(identity *flow.Identity) { 201 // creates an assigner engine for non-active verification node. 202 s := SetupTest(WithIdentity(identity)) 203 e := NewAssignerEngine(s) 204 205 // creates a container block, with a single receipt, that contains 206 // no assigned chunk to verification node. 207 containerBlock, _ := createContainerBlock( 208 vertestutils.WithChunks( // all chunks assigned to some (random) identifiers, but not this verification node 209 vertestutils.WithAssignee(unittest.IdentifierFixture()), 210 vertestutils.WithAssignee(unittest.IdentifierFixture()), 211 vertestutils.WithAssignee(unittest.IdentifierFixture()))) 212 result := containerBlock.Payload.Results[0] 213 s.mockStateAtBlockID(result.BlockID) 214 215 // once assigner engine is done processing the block, it should notify the processing notifier. 216 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 217 218 // sends block containing receipt to assigner engine 219 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 220 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 221 e.ProcessFinalizedBlock(containerBlock) 222 223 // when the node has zero-weight at reference block id, chunk assigner should not be called, 224 // and nothing should be passed to chunks queue, and 225 // job listener should not be notified. 226 s.chunksQueue.AssertNotCalled(t, "StoreChunkLocator") 227 s.newChunkListener.AssertNotCalled(t, "Check") 228 s.assigner.AssertNotCalled(t, "Assign") 229 230 mock.AssertExpectationsForObjects(t, 231 s.metrics, 232 s.assigner, 233 s.notifier) 234 } 235 236 t.Run("verifier-joining", func(t *testing.T) { 237 identity := unittest.IdentityFixture( 238 unittest.WithRole(flow.RoleVerification), 239 unittest.WithParticipationStatus(flow.EpochParticipationStatusJoining), 240 ) 241 assertIdentityAtReferenceBlock(identity) 242 }) 243 t.Run("verifier-leaving", func(t *testing.T) { 244 identity := unittest.IdentityFixture( 245 unittest.WithRole(flow.RoleVerification), 246 unittest.WithParticipationStatus(flow.EpochParticipationStatusLeaving), 247 ) 248 assertIdentityAtReferenceBlock(identity) 249 }) 250 t.Run("verifier-zero-weight", func(t *testing.T) { 251 identity := unittest.IdentityFixture( 252 unittest.WithRole(flow.RoleVerification), 253 unittest.WithInitialWeight(0), 254 ) 255 assertIdentityAtReferenceBlock(identity) 256 }) 257 } 258 259 // newBlockNoChunk evaluates passing a new finalized block to assigner engine that contains 260 // a receipt with no chunk in its result. Assigner engine should 261 // not pass any chunk to the chunks queue, and should never notify the job listener. 262 func newBlockNoChunk(t *testing.T) { 263 s := SetupTest() 264 e := NewAssignerEngine(s) 265 266 // creates a container block, with a single receipt, that contains no chunks. 267 containerBlock, assignment := createContainerBlock() 268 result := containerBlock.Payload.Results[0] 269 s.mockStateAtBlockID(result.BlockID) 270 chunksNum := s.mockChunkAssigner(flow.NewIncorporatedResult(containerBlock.ID(), result), assignment) 271 require.Equal(t, chunksNum, 0) // no chunk should be assigned 272 273 // once assigner engine is done processing the block, it should notify the processing notifier. 274 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 275 276 // sends block containing receipt to assigner engine 277 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 278 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 279 e.ProcessFinalizedBlock(containerBlock) 280 281 mock.AssertExpectationsForObjects(t, 282 s.metrics, 283 s.assigner, 284 s.notifier) 285 286 // when there is no chunk, nothing should be passed to chunks queue, and 287 // job listener should not be notified. 288 s.chunksQueue.AssertNotCalled(t, "StoreChunkLocator") 289 s.newChunkListener.AssertNotCalled(t, "Check") 290 } 291 292 // newBlockNoAssignedChunk evaluates passing a new finalized block to assigner engine that contains 293 // a receipt with no assigned chunk for the verification node in its result. Assigner engine should 294 // not pass any chunk to the chunks queue, and should not notify the job listener. 295 func newBlockNoAssignedChunk(t *testing.T) { 296 s := SetupTest() 297 e := NewAssignerEngine(s) 298 299 // creates a container block, with a single receipt, that contains 5 chunks, but 300 // none of them is assigned to this verification node. 301 containerBlock, assignment := createContainerBlock( 302 vertestutils.WithChunks( 303 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 304 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 305 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 306 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 307 vertestutils.WithAssignee(unittest.IdentifierFixture()))) // assigned to others 308 result := containerBlock.Payload.Results[0] 309 s.mockStateAtBlockID(result.BlockID) 310 chunksNum := s.mockChunkAssigner(flow.NewIncorporatedResult(containerBlock.ID(), result), assignment) 311 require.Equal(t, chunksNum, 0) // no chunk should be assigned 312 313 // once assigner engine is done processing the block, it should notify the processing notifier. 314 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 315 316 // sends block containing receipt to assigner engine 317 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 318 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 319 e.ProcessFinalizedBlock(containerBlock) 320 321 mock.AssertExpectationsForObjects(t, 322 s.metrics, 323 s.assigner, 324 s.notifier) 325 326 // when there is no assigned chunk, nothing should be passed to chunks queue, and 327 // job listener should not be notified. 328 s.chunksQueue.AssertNotCalled(t, "StoreChunkLocator") 329 s.newChunkListener.AssertNotCalled(t, "Check") 330 } 331 332 // newBlockMultipleAssignment evaluates that passing a new finalized block to assigner engine that contains 333 // a receipt with multiple assigned chunk, results in the assigner engine passing all assigned chunks to the 334 // chunks queue and notifying the job listener of the assigned chunks. 335 func newBlockMultipleAssignment(t *testing.T) { 336 s := SetupTest() 337 e := NewAssignerEngine(s) 338 339 // creates a container block, with a single receipt, that contains 5 chunks, but 340 // only 3 of them is assigned to this verification node. 341 containerBlock, assignment := createContainerBlock( 342 vertestutils.WithChunks( 343 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 344 vertestutils.WithAssignee(s.myID()), // assigned to me 345 vertestutils.WithAssignee(s.myID()), // assigned to me 346 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 347 vertestutils.WithAssignee(s.myID()))) // assigned to me 348 result := containerBlock.Payload.Results[0] 349 s.mockStateAtBlockID(result.BlockID) 350 chunksNum := s.mockChunkAssigner(flow.NewIncorporatedResult(containerBlock.ID(), result), assignment) 351 require.Equal(t, chunksNum, 3) // 3 chunks should be assigned 352 353 // mocks processing assigned chunks 354 // each assigned chunk should be stored in the chunks queue and new chunk listener should be 355 // invoked for it. 356 chunksQueueWG := mockChunksQueueForAssignment(t, s.verIdentity.NodeID, s.chunksQueue, result.ID(), assignment, true, nil) 357 s.newChunkListener.On("Check").Return().Times(chunksNum) 358 s.metrics.On("OnAssignedChunkProcessedAtAssigner").Return().Times(chunksNum) 359 360 // once assigner engine is done processing the block, it should notify the processing notifier. 361 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 362 363 // sends containerBlock containing receipt to assigner engine 364 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 365 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 366 e.ProcessFinalizedBlock(containerBlock) 367 368 unittest.RequireReturnsBefore(t, chunksQueueWG.Wait, 10*time.Millisecond, "could not receive chunk locators") 369 370 mock.AssertExpectationsForObjects(t, 371 s.metrics, 372 s.assigner, 373 s.notifier, 374 s.newChunkListener) 375 } 376 377 // chunkQueueUnhappyPathDuplicate evaluates that after submitting duplicate chunk to chunk queue, assigner engine does not invoke the notifier. 378 // This is important as without a new chunk successfully added to the chunks queue, the consumer should not be notified. 379 func chunkQueueUnhappyPathDuplicate(t *testing.T) { 380 s := SetupTest() 381 e := NewAssignerEngine(s) 382 383 // creates a container block, with a single receipt, that contains a single chunk assigned 384 // to verification node. 385 containerBlock, assignment := createContainerBlock( 386 vertestutils.WithChunks(vertestutils.WithAssignee(s.myID()))) 387 result := containerBlock.Payload.Results[0] 388 s.mockStateAtBlockID(result.BlockID) 389 chunksNum := s.mockChunkAssigner(flow.NewIncorporatedResult(containerBlock.ID(), result), assignment) 390 require.Equal(t, chunksNum, 1) 391 392 // mocks processing assigned chunks 393 // adding new chunks to queue returns false, which means a duplicate chunk. 394 chunksQueueWG := mockChunksQueueForAssignment(t, s.verIdentity.NodeID, s.chunksQueue, result.ID(), assignment, false, nil) 395 396 // once assigner engine is done processing the block, it should notify the processing notifier. 397 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 398 399 // sends block containing receipt to assigner engine 400 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 401 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 402 e.ProcessFinalizedBlock(containerBlock) 403 404 unittest.RequireReturnsBefore(t, chunksQueueWG.Wait, 10*time.Millisecond, "could not receive chunk locators") 405 406 mock.AssertExpectationsForObjects(t, 407 s.metrics, 408 s.assigner, 409 s.notifier) 410 411 // job listener should not be notified as no new chunk is added. 412 s.newChunkListener.AssertNotCalled(t, "Check") 413 } 414 415 // mockChunksQueueForAssignment mocks chunks queue against invoking its store functionality for the 416 // input assignment. 417 // The mocked version of chunks queue evaluates that whatever chunk locator is tried to be stored belongs to the 418 // assigned list of chunks for specified execution result (i.e., a valid input). 419 // It also mocks the chunks queue to return the specified boolean and error values upon trying to store a valid input. 420 func mockChunksQueueForAssignment(t *testing.T, 421 verId flow.Identifier, 422 chunksQueue *storage.ChunksQueue, 423 resultID flow.Identifier, 424 assignment *chunks.Assignment, 425 returnBool bool, 426 returnError error) *sync.WaitGroup { 427 428 wg := &sync.WaitGroup{} 429 wg.Add(len(assignment.ByNodeID(verId))) 430 chunksQueue.On("StoreChunkLocator", mock.Anything).Run(func(args mock.Arguments) { 431 // should be a chunk locator 432 locator, ok := args[0].(*chunks.Locator) 433 require.True(t, ok) 434 435 // should belong to the expected execution result and assigned chunk 436 require.Equal(t, resultID, locator.ResultID) 437 require.Contains(t, assignment.ByNodeID(verId), locator.Index) 438 439 wg.Done() 440 }).Return(returnBool, returnError) 441 442 return wg 443 }