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  }