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  }