github.com/koko1123/flow-go-1@v0.29.6/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/koko1123/flow-go-1/engine/verification/assigner" 13 vertestutils "github.com/koko1123/flow-go-1/engine/verification/utils/unittest" 14 "github.com/koko1123/flow-go-1/model/chunks" 15 "github.com/koko1123/flow-go-1/model/flow" 16 module "github.com/koko1123/flow-go-1/module/mock" 17 "github.com/koko1123/flow-go-1/module/trace" 18 protocol "github.com/koko1123/flow-go-1/state/protocol/mock" 19 storage "github.com/koko1123/flow-go-1/storage/mock" 20 "github.com/koko1123/flow-go-1/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 zero-weight", func(t *testing.T) { 135 newBlockZeroWeight(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 // newBlockZeroWeight evaluates that when verification node has zero weight at a reference block, 193 // it drops the corresponding execution receipts for that block without performing any chunk assignment. 194 // It also evaluates that the chunks queue is never called on any chunks of that receipt's result. 195 func newBlockZeroWeight(t *testing.T) { 196 197 // creates an assigner engine for zero-weight verification node. 198 s := SetupTest(WithIdentity( 199 unittest.IdentityFixture( 200 unittest.WithRole(flow.RoleVerification), 201 unittest.WithWeight(0)))) 202 e := NewAssignerEngine(s) 203 204 // creates a container block, with a single receipt, that contains 205 // no assigned chunk to verification node. 206 containerBlock, _ := createContainerBlock( 207 vertestutils.WithChunks( // all chunks assigned to some (random) identifiers, but not this verification node 208 vertestutils.WithAssignee(unittest.IdentifierFixture()), 209 vertestutils.WithAssignee(unittest.IdentifierFixture()), 210 vertestutils.WithAssignee(unittest.IdentifierFixture()))) 211 result := containerBlock.Payload.Results[0] 212 s.mockStateAtBlockID(result.BlockID) 213 214 // once assigner engine is done processing the block, it should notify the processing notifier. 215 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 216 217 // sends block containing receipt to assigner engine 218 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 219 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 220 e.ProcessFinalizedBlock(containerBlock) 221 222 // when the node has zero-weight at reference block id, chunk assigner should not be called, 223 // and nothing should be passed to chunks queue, and 224 // job listener should not be notified. 225 s.chunksQueue.AssertNotCalled(t, "StoreChunkLocator") 226 s.newChunkListener.AssertNotCalled(t, "Check") 227 s.assigner.AssertNotCalled(t, "Assign") 228 229 mock.AssertExpectationsForObjects(t, 230 s.metrics, 231 s.assigner, 232 s.notifier) 233 } 234 235 // newBlockNoChunk evaluates passing a new finalized block to assigner engine that contains 236 // a receipt with no chunk in its result. Assigner engine should 237 // not pass any chunk to the chunks queue, and should never notify the job listener. 238 func newBlockNoChunk(t *testing.T) { 239 s := SetupTest() 240 e := NewAssignerEngine(s) 241 242 // creates a container block, with a single receipt, that contains no chunks. 243 containerBlock, assignment := createContainerBlock() 244 result := containerBlock.Payload.Results[0] 245 s.mockStateAtBlockID(result.BlockID) 246 chunksNum := s.mockChunkAssigner(flow.NewIncorporatedResult(containerBlock.ID(), result), assignment) 247 require.Equal(t, chunksNum, 0) // no chunk should be assigned 248 249 // once assigner engine is done processing the block, it should notify the processing notifier. 250 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 251 252 // sends block containing receipt to assigner engine 253 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 254 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 255 e.ProcessFinalizedBlock(containerBlock) 256 257 mock.AssertExpectationsForObjects(t, 258 s.metrics, 259 s.assigner, 260 s.notifier) 261 262 // when there is no chunk, nothing should be passed to chunks queue, and 263 // job listener should not be notified. 264 s.chunksQueue.AssertNotCalled(t, "StoreChunkLocator") 265 s.newChunkListener.AssertNotCalled(t, "Check") 266 } 267 268 // newBlockNoAssignedChunk evaluates passing a new finalized block to assigner engine that contains 269 // a receipt with no assigned chunk for the verification node in its result. Assigner engine should 270 // not pass any chunk to the chunks queue, and should not notify the job listener. 271 func newBlockNoAssignedChunk(t *testing.T) { 272 s := SetupTest() 273 e := NewAssignerEngine(s) 274 275 // creates a container block, with a single receipt, that contains 5 chunks, but 276 // none of them is assigned to this verification node. 277 containerBlock, assignment := createContainerBlock( 278 vertestutils.WithChunks( 279 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 280 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 281 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 282 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 283 vertestutils.WithAssignee(unittest.IdentifierFixture()))) // assigned to others 284 result := containerBlock.Payload.Results[0] 285 s.mockStateAtBlockID(result.BlockID) 286 chunksNum := s.mockChunkAssigner(flow.NewIncorporatedResult(containerBlock.ID(), result), assignment) 287 require.Equal(t, chunksNum, 0) // no chunk should be assigned 288 289 // once assigner engine is done processing the block, it should notify the processing notifier. 290 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 291 292 // sends block containing receipt to assigner engine 293 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 294 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 295 e.ProcessFinalizedBlock(containerBlock) 296 297 mock.AssertExpectationsForObjects(t, 298 s.metrics, 299 s.assigner, 300 s.notifier) 301 302 // when there is no assigned chunk, nothing should be passed to chunks queue, and 303 // job listener should not be notified. 304 s.chunksQueue.AssertNotCalled(t, "StoreChunkLocator") 305 s.newChunkListener.AssertNotCalled(t, "Check") 306 } 307 308 // newBlockMultipleAssignment evaluates that passing a new finalized block to assigner engine that contains 309 // a receipt with multiple assigned chunk, results in the assigner engine passing all assigned chunks to the 310 // chunks queue and notifying the job listener of the assigned chunks. 311 func newBlockMultipleAssignment(t *testing.T) { 312 s := SetupTest() 313 e := NewAssignerEngine(s) 314 315 // creates a container block, with a single receipt, that contains 5 chunks, but 316 // only 3 of them is assigned to this verification node. 317 containerBlock, assignment := createContainerBlock( 318 vertestutils.WithChunks( 319 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 320 vertestutils.WithAssignee(s.myID()), // assigned to me 321 vertestutils.WithAssignee(s.myID()), // assigned to me 322 vertestutils.WithAssignee(unittest.IdentifierFixture()), // assigned to others 323 vertestutils.WithAssignee(s.myID()))) // assigned to me 324 result := containerBlock.Payload.Results[0] 325 s.mockStateAtBlockID(result.BlockID) 326 chunksNum := s.mockChunkAssigner(flow.NewIncorporatedResult(containerBlock.ID(), result), assignment) 327 require.Equal(t, chunksNum, 3) // 3 chunks should be assigned 328 329 // mocks processing assigned chunks 330 // each assigned chunk should be stored in the chunks queue and new chunk listener should be 331 // invoked for it. 332 chunksQueueWG := mockChunksQueueForAssignment(t, s.verIdentity.NodeID, s.chunksQueue, result.ID(), assignment, true, nil) 333 s.newChunkListener.On("Check").Return().Times(chunksNum) 334 s.metrics.On("OnAssignedChunkProcessedAtAssigner").Return().Times(chunksNum) 335 336 // once assigner engine is done processing the block, it should notify the processing notifier. 337 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 338 339 // sends containerBlock containing receipt to assigner engine 340 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 341 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 342 e.ProcessFinalizedBlock(containerBlock) 343 344 unittest.RequireReturnsBefore(t, chunksQueueWG.Wait, 10*time.Millisecond, "could not receive chunk locators") 345 346 mock.AssertExpectationsForObjects(t, 347 s.metrics, 348 s.assigner, 349 s.notifier, 350 s.newChunkListener) 351 } 352 353 // chunkQueueUnhappyPathDuplicate evaluates that after submitting duplicate chunk to chunk queue, assigner engine does not invoke the notifier. 354 // This is important as without a new chunk successfully added to the chunks queue, the consumer should not be notified. 355 func chunkQueueUnhappyPathDuplicate(t *testing.T) { 356 s := SetupTest() 357 e := NewAssignerEngine(s) 358 359 // creates a container block, with a single receipt, that contains a single chunk assigned 360 // to verification node. 361 containerBlock, assignment := createContainerBlock( 362 vertestutils.WithChunks(vertestutils.WithAssignee(s.myID()))) 363 result := containerBlock.Payload.Results[0] 364 s.mockStateAtBlockID(result.BlockID) 365 chunksNum := s.mockChunkAssigner(flow.NewIncorporatedResult(containerBlock.ID(), result), assignment) 366 require.Equal(t, chunksNum, 1) 367 368 // mocks processing assigned chunks 369 // adding new chunks to queue returns false, which means a duplicate chunk. 370 chunksQueueWG := mockChunksQueueForAssignment(t, s.verIdentity.NodeID, s.chunksQueue, result.ID(), assignment, false, nil) 371 372 // once assigner engine is done processing the block, it should notify the processing notifier. 373 s.notifier.On("Notify", containerBlock.ID()).Return().Once() 374 375 // sends block containing receipt to assigner engine 376 s.metrics.On("OnFinalizedBlockArrivedAtAssigner", containerBlock.Header.Height).Return().Once() 377 s.metrics.On("OnExecutionResultReceivedAtAssignerEngine").Return().Once() 378 e.ProcessFinalizedBlock(containerBlock) 379 380 unittest.RequireReturnsBefore(t, chunksQueueWG.Wait, 10*time.Millisecond, "could not receive chunk locators") 381 382 mock.AssertExpectationsForObjects(t, 383 s.metrics, 384 s.assigner, 385 s.notifier) 386 387 // job listener should not be notified as no new chunk is added. 388 s.newChunkListener.AssertNotCalled(t, "Check") 389 } 390 391 // mockChunksQueueForAssignment mocks chunks queue against invoking its store functionality for the 392 // input assignment. 393 // The mocked version of chunks queue evaluates that whatever chunk locator is tried to be stored belongs to the 394 // assigned list of chunks for specified execution result (i.e., a valid input). 395 // It also mocks the chunks queue to return the specified boolean and error values upon trying to store a valid input. 396 func mockChunksQueueForAssignment(t *testing.T, 397 verId flow.Identifier, 398 chunksQueue *storage.ChunksQueue, 399 resultID flow.Identifier, 400 assignment *chunks.Assignment, 401 returnBool bool, 402 returnError error) *sync.WaitGroup { 403 404 wg := &sync.WaitGroup{} 405 wg.Add(len(assignment.ByNodeID(verId))) 406 chunksQueue.On("StoreChunkLocator", mock.Anything).Run(func(args mock.Arguments) { 407 // should be a chunk locator 408 locator, ok := args[0].(*chunks.Locator) 409 require.True(t, ok) 410 411 // should belong to the expected execution result and assigned chunk 412 require.Equal(t, resultID, locator.ResultID) 413 require.Contains(t, assignment.ByNodeID(verId), locator.Index) 414 415 wg.Done() 416 }).Return(returnBool, returnError) 417 418 return wg 419 }