github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/verification/requester/requester_test.go (about)

     1  package requester_test
     2  
     3  import (
     4  	"sync"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/rs/zerolog"
     9  	testifymock "github.com/stretchr/testify/mock"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	mockfetcher "github.com/onflow/flow-go/engine/verification/fetcher/mock"
    13  	"github.com/onflow/flow-go/engine/verification/requester"
    14  	vertestutils "github.com/onflow/flow-go/engine/verification/utils/unittest"
    15  	"github.com/onflow/flow-go/model/chunks"
    16  	"github.com/onflow/flow-go/model/flow"
    17  	"github.com/onflow/flow-go/model/messages"
    18  	"github.com/onflow/flow-go/model/verification"
    19  	"github.com/onflow/flow-go/module"
    20  	flowmempool "github.com/onflow/flow-go/module/mempool"
    21  	mempool "github.com/onflow/flow-go/module/mempool/mock"
    22  	"github.com/onflow/flow-go/module/mock"
    23  	"github.com/onflow/flow-go/module/trace"
    24  	"github.com/onflow/flow-go/network/channels"
    25  	"github.com/onflow/flow-go/network/mocknetwork"
    26  	protocol "github.com/onflow/flow-go/state/protocol/mock"
    27  	"github.com/onflow/flow-go/utils/unittest"
    28  )
    29  
    30  // RequesterEngineTestSuite encapsulates data structures for running unittests on requester engine.
    31  type RequesterEngineTestSuite struct {
    32  	// modules
    33  	log             zerolog.Logger
    34  	handler         *mockfetcher.ChunkDataPackHandler // contains callbacks for handling received chunk data packs.
    35  	pendingRequests *mempool.ChunkRequests            // used to store all the pending chunks that assigned to this node
    36  	state           *protocol.State                   // used to check the last sealed height
    37  	con             *mocknetwork.Conduit              // used to send chunk data request, and receive the response
    38  	tracer          module.Tracer
    39  	metrics         *mock.VerificationMetrics
    40  
    41  	// identities
    42  	verIdentity *flow.Identity // verification node
    43  
    44  	// parameters
    45  	requestTargets uint64
    46  	retryInterval  time.Duration // determines time in milliseconds for retrying chunk data requests.
    47  }
    48  
    49  // setupTest initiates a test suite prior to each test.
    50  func setupTest() *RequesterEngineTestSuite {
    51  	r := &RequesterEngineTestSuite{
    52  		log:             unittest.Logger(),
    53  		tracer:          trace.NewNoopTracer(),
    54  		metrics:         &mock.VerificationMetrics{},
    55  		handler:         &mockfetcher.ChunkDataPackHandler{},
    56  		retryInterval:   100 * time.Millisecond,
    57  		requestTargets:  2,
    58  		pendingRequests: &mempool.ChunkRequests{},
    59  		state:           &protocol.State{},
    60  		verIdentity:     unittest.IdentityFixture(unittest.WithRole(flow.RoleVerification)),
    61  		con:             &mocknetwork.Conduit{},
    62  	}
    63  
    64  	return r
    65  }
    66  
    67  // newRequesterEngine returns a requester engine for testing.
    68  func newRequesterEngine(t *testing.T, s *RequesterEngineTestSuite) *requester.Engine {
    69  	net := &mocknetwork.Network{}
    70  	// mocking the network registration of the engine
    71  	net.On("Register", channels.RequestChunks, testifymock.Anything).
    72  		Return(s.con, nil).
    73  		Once()
    74  
    75  	e, err := requester.New(s.log,
    76  		s.state,
    77  		net,
    78  		s.tracer,
    79  		s.metrics,
    80  		s.pendingRequests,
    81  		s.retryInterval,
    82  		// requests are only qualified if their retryAfter is elapsed.
    83  		requester.RetryAfterQualifier,
    84  		// exponential backoff with multiplier of 2, minimum interval of a second, and
    85  		// maximum interval of an hour.
    86  		flowmempool.ExponentialUpdater(2, time.Hour, time.Second),
    87  		s.requestTargets)
    88  	require.NoError(t, err)
    89  	testifymock.AssertExpectationsForObjects(t, net)
    90  
    91  	e.WithChunkDataPackHandler(s.handler)
    92  
    93  	return e
    94  }
    95  
    96  // TestHandleChunkDataPack_Request evaluates happy path of submitting a request to requester engine.
    97  // The request is added to pending request mempools, and metrics updated.
    98  func TestHandleChunkDataPack_Request(t *testing.T) {
    99  	s := setupTest()
   100  	e := newRequesterEngine(t, s)
   101  
   102  	request := unittest.ChunkDataPackRequestFixture(unittest.WithChunkID(unittest.IdentifierFixture()))
   103  	s.pendingRequests.On("Add", request).Return(true).Once()
   104  	s.metrics.On("OnChunkDataPackRequestReceivedByRequester").Return().Once()
   105  
   106  	e.Request(request)
   107  
   108  	testifymock.AssertExpectationsForObjects(t, s.pendingRequests, s.metrics)
   109  }
   110  
   111  // TestHandleChunkDataPack_HappyPath evaluates the happy path of receiving a requested chunk data pack.
   112  // The chunk data pack should be passed to the registered handler, and the resources should be cleaned up.
   113  func TestHandleChunkDataPack_HappyPath(t *testing.T) {
   114  	s := setupTest()
   115  	e := newRequesterEngine(t, s)
   116  
   117  	response := unittest.ChunkDataResponseMsgFixture(unittest.IdentifierFixture())
   118  	request := unittest.ChunkDataPackRequestFixture(unittest.WithChunkID(response.ChunkDataPack.ChunkID))
   119  	originID := unittest.IdentifierFixture()
   120  
   121  	// we remove pending request on receiving this response
   122  	locators := chunks.LocatorMap{}
   123  	locators[chunks.ChunkLocatorID(request.ResultID, request.Index)] = &chunks.Locator{
   124  		ResultID: request.ResultID,
   125  		Index:    request.Index,
   126  	}
   127  	s.pendingRequests.On("PopAll", response.ChunkDataPack.ChunkID).Return(locators, true).Once()
   128  
   129  	s.handler.On("HandleChunkDataPack", originID, &verification.ChunkDataPackResponse{
   130  		Locator: chunks.Locator{
   131  			ResultID: request.ResultID,
   132  			Index:    request.Index,
   133  		},
   134  		Cdp: &response.ChunkDataPack,
   135  	}).Return().Once()
   136  	s.metrics.On("OnChunkDataPackResponseReceivedFromNetworkByRequester").Return().Once()
   137  	s.metrics.On("OnChunkDataPackSentToFetcher").Return().Once()
   138  
   139  	err := e.Process(channels.RequestChunks, originID, response)
   140  	require.Nil(t, err)
   141  
   142  	testifymock.AssertExpectationsForObjects(t, s.con, s.handler, s.pendingRequests, s.metrics)
   143  }
   144  
   145  // TestHandleChunkDataPack_HappyPath_Multiple evaluates the happy path of receiving several requested chunk data packs.
   146  // Each chunk data pack should be handled once by being passed to the registered handler,
   147  // the chunk ID and collection ID should match the response, and the resources should be cleaned up.
   148  func TestHandleChunkDataPack_HappyPath_Multiple(t *testing.T) {
   149  	s := setupTest()
   150  	e := newRequesterEngine(t, s)
   151  
   152  	// creates list of chunk data pack responses
   153  	count := 10
   154  	requests := unittest.ChunkDataPackRequestListFixture(count)
   155  	originID := unittest.IdentifierFixture()
   156  	chunkIDs := toChunkIDs(t, requests)
   157  	responses := unittest.ChunkDataResponseMessageListFixture(chunkIDs)
   158  
   159  	// we remove pending request on receiving this response
   160  	mockPendingRequestsPopAll(t, s.pendingRequests, requests)
   161  	// we pass each chunk data pack and its collection to chunk data pack handler
   162  	handlerWG := mockChunkDataPackHandler(t, s.handler, requests)
   163  
   164  	s.metrics.On("OnChunkDataPackResponseReceivedFromNetworkByRequester").Return().Times(len(responses))
   165  	s.metrics.On("OnChunkDataPackSentToFetcher").Return().Times(len(responses))
   166  
   167  	for _, response := range responses {
   168  		err := e.Process(channels.RequestChunks, originID, response)
   169  		require.Nil(t, err)
   170  	}
   171  
   172  	unittest.RequireReturnsBefore(t, handlerWG.Wait, 100*time.Millisecond, "could not handle chunk data responses on time")
   173  	testifymock.AssertExpectationsForObjects(t, s.con, s.metrics)
   174  }
   175  
   176  // TestHandleChunkDataPack_NonExistingRequest evaluates that failing to remove a received chunk data pack's request
   177  // from the memory terminates the procedure of handling a chunk data pack without passing it to the handler.
   178  // The request for a chunk data pack may be removed from the memory if duplicate copies of a requested chunk data pack arrive
   179  // concurrently. Then the mutex lock on pending requests mempool allows only one of those requested chunk data packs to remove the
   180  // request and pass to handler. While handling the other ones gracefully terminated.
   181  func TestHandleChunkDataPack_FailedRequestRemoval(t *testing.T) {
   182  	s := setupTest()
   183  	e := newRequesterEngine(t, s)
   184  
   185  	response := unittest.ChunkDataResponseMsgFixture(unittest.IdentifierFixture())
   186  	originID := unittest.IdentifierFixture()
   187  
   188  	// however by the time we try remove it, the request has gone.
   189  	// this can happen when duplicate chunk data packs are coming concurrently.
   190  	// the concurrency is safe with pending requests mempool's mutex lock.
   191  	s.pendingRequests.On("PopAll", response.ChunkDataPack.ChunkID).Return(nil, false).Once()
   192  	s.metrics.On("OnChunkDataPackResponseReceivedFromNetworkByRequester").Return().Once()
   193  
   194  	err := e.Process(channels.RequestChunks, originID, response)
   195  	require.Nil(t, err)
   196  
   197  	testifymock.AssertExpectationsForObjects(t, s.pendingRequests, s.con, s.metrics)
   198  	s.handler.AssertNotCalled(t, "HandleChunkDataPack")
   199  }
   200  
   201  // TestRequestPendingChunkSealedBlock evaluates that requester engine drops pending requests for chunks belonging to
   202  // sealed blocks, and also notifies the handler that this requested chunk has been sealed, so it no longer requests
   203  // from the network it.
   204  func TestRequestPendingChunkSealedBlock(t *testing.T) {
   205  	s := setupTest()
   206  	e := newRequesterEngine(t, s)
   207  
   208  	// creates a single chunk request that belongs to a sealed height.
   209  	agrees := unittest.IdentifierListFixture(2)
   210  	disagrees := unittest.IdentifierListFixture(3)
   211  	requests := unittest.ChunkDataPackRequestListFixture(1,
   212  		unittest.WithHeight(5),
   213  		unittest.WithAgrees(agrees),
   214  		unittest.WithDisagrees(disagrees))
   215  	vertestutils.MockLastSealedHeight(s.state, 10)
   216  	s.pendingRequests.On("All").Return(requests.UniqueRequestInfo())
   217  	// check data pack request is never tried since its block has been sealed.
   218  	s.metrics.On("SetMaxChunkDataPackAttemptsForNextUnsealedHeightAtRequester", uint64(0)).Return().Once()
   219  
   220  	unittest.RequireCloseBefore(t, e.Ready(), time.Second, "could not start engine on time")
   221  
   222  	mockPendingRequestsPopAll(t, s.pendingRequests, requests)
   223  	notifierWG := mockNotifyBlockSealedHandler(t, s.handler, requests)
   224  
   225  	unittest.RequireReturnsBefore(t, notifierWG.Wait, time.Duration(2)*s.retryInterval, "could not notify the handler on time")
   226  
   227  	unittest.RequireCloseBefore(t, e.Done(), time.Second, "could not stop engine on time")
   228  	// requester does not call publish to disseminate the request for this chunk.
   229  	s.con.AssertNotCalled(t, "Publish")
   230  }
   231  
   232  // TestCompleteRequestingUnsealedChunkCycle evaluates a complete life cycle of receiving a chunk request by the requester.
   233  // The requester should submit the request to the network (on its timer overflow), and receive the response back and send it to
   234  // the registered handler.
   235  //
   236  // It should also clean the request from memory.
   237  func TestCompleteRequestingUnsealedChunkLifeCycle(t *testing.T) {
   238  	s := setupTest()
   239  	e := newRequesterEngine(t, s)
   240  
   241  	sealedHeight := uint64(10)
   242  	// Creates a single chunk request with its corresponding response.
   243  	// The chunk belongs to an unsealed block.
   244  	agrees := unittest.IdentifierListFixture(2)
   245  	disagrees := unittest.IdentifierListFixture(3)
   246  	requests := unittest.ChunkDataPackRequestListFixture(1,
   247  		unittest.WithHeightGreaterThan(sealedHeight),
   248  		unittest.WithAgrees(agrees),
   249  		unittest.WithDisagrees(disagrees))
   250  	response := unittest.ChunkDataResponseMsgFixture(requests[0].ChunkID)
   251  
   252  	// mocks the requester pipeline
   253  	vertestutils.MockLastSealedHeight(s.state, sealedHeight)
   254  	s.pendingRequests.On("All").Return(requests.UniqueRequestInfo())
   255  	handlerWG := mockChunkDataPackHandler(t, s.handler, requests)
   256  	mockPendingRequestsPopAll(t, s.pendingRequests, requests)
   257  
   258  	// makes all chunk requests being qualified for dispatch instantly
   259  	requestHistoryWG, updateHistoryWG := mockPendingRequestInfoAndUpdate(t,
   260  		s.pendingRequests,
   261  		requests,
   262  		verification.ChunkDataPackRequestList{},
   263  		verification.ChunkDataPackRequestList{},
   264  		1)
   265  	s.metrics.On("OnChunkDataPackResponseReceivedFromNetworkByRequester").Return().Times(len(requests))
   266  	s.metrics.On("OnChunkDataPackRequestDispatchedInNetworkByRequester").Return().Times(len(requests))
   267  	s.metrics.On("OnChunkDataPackSentToFetcher").Return().Times(len(requests))
   268  	s.metrics.On("SetMaxChunkDataPackAttemptsForNextUnsealedHeightAtRequester", uint64(1)).Return().Once()
   269  
   270  	unittest.RequireCloseBefore(t, e.Ready(), time.Second, "could not start engine on time")
   271  
   272  	// we wait till the engine submits the chunk request to the network, and receive the response
   273  	conduitWG := mockConduitForChunkDataPackRequest(t, s.con, requests, 1, func(request *messages.ChunkDataRequest) {
   274  		err := e.Process(channels.RequestChunks, requests[0].Agrees[0], response)
   275  		require.NoError(t, err)
   276  	})
   277  	unittest.RequireReturnsBefore(t, requestHistoryWG.Wait, time.Duration(2)*s.retryInterval, "could not check chunk requests qualification on time")
   278  	unittest.RequireReturnsBefore(t, updateHistoryWG.Wait, s.retryInterval, "could not update chunk request history on time")
   279  	unittest.RequireReturnsBefore(t, conduitWG.Wait, time.Duration(2)*s.retryInterval, "could not request chunks from network")
   280  	unittest.RequireReturnsBefore(t, handlerWG.Wait, 100*time.Second, "could not handle chunk data responses on time")
   281  
   282  	unittest.RequireCloseBefore(t, e.Done(), time.Second, "could not stop engine on time")
   283  	testifymock.AssertExpectationsForObjects(t, s.metrics)
   284  }
   285  
   286  // TestRequestPendingChunkSealedBlock_Hybrid evaluates the situation that requester has some pending chunk requests belonging to sealed blocks
   287  // (i.e., sealed chunks), and some pending chunk requests belonging to unsealed blocks (i.e., unsealed chunks).
   288  //
   289  // On timer, the requester should submit pending requests for unsealed chunks to the network, while dropping the requests for the
   290  // sealed chunks, and notify the handler.
   291  func TestRequestPendingChunkSealedBlock_Hybrid(t *testing.T) {
   292  	s := setupTest()
   293  	e := newRequesterEngine(t, s)
   294  
   295  	sealedHeight := uint64(10)
   296  	// creates 2 chunk data packs that belong to a sealed height, and
   297  	// 3 that belong to an unsealed height.
   298  	agrees := unittest.IdentifierListFixture(2)
   299  	disagrees := unittest.IdentifierListFixture(3)
   300  	sealedRequests := unittest.ChunkDataPackRequestListFixture(2,
   301  		unittest.WithHeight(sealedHeight-1),
   302  		unittest.WithAgrees(agrees),
   303  		unittest.WithDisagrees(disagrees))
   304  	unsealedRequests := unittest.ChunkDataPackRequestListFixture(3,
   305  		unittest.WithHeightGreaterThan(sealedHeight),
   306  		unittest.WithAgrees(agrees),
   307  		unittest.WithDisagrees(disagrees))
   308  	requests := append(sealedRequests, unsealedRequests...)
   309  
   310  	vertestutils.MockLastSealedHeight(s.state, sealedHeight)
   311  	s.pendingRequests.On("All").Return(requests.UniqueRequestInfo())
   312  
   313  	// makes all (unsealed) chunk requests being qualified for dispatch instantly
   314  	requestHistoryWG, updateHistoryWG := mockPendingRequestInfoAndUpdate(t,
   315  		s.pendingRequests,
   316  		unsealedRequests,
   317  		verification.ChunkDataPackRequestList{},
   318  		verification.ChunkDataPackRequestList{},
   319  		1)
   320  	s.metrics.On("OnChunkDataPackRequestDispatchedInNetworkByRequester").Return().Times(len(unsealedRequests))
   321  	// each unsealed height is requested only once, hence the maximum is updated only once from 0 -> 1
   322  	s.metrics.On("SetMaxChunkDataPackAttemptsForNextUnsealedHeightAtRequester", testifymock.Anything).Return().Once()
   323  
   324  	unittest.RequireCloseBefore(t, e.Ready(), time.Second, "could not start engine on time")
   325  
   326  	// sealed requests should be removed and the handler should be notified.
   327  	mockPendingRequestsPopAll(t, s.pendingRequests, sealedRequests)
   328  	notifierWG := mockNotifyBlockSealedHandler(t, s.handler, sealedRequests)
   329  	// unsealed requests should be submitted to the network once
   330  	conduitWG := mockConduitForChunkDataPackRequest(t, s.con, unsealedRequests, 1, func(*messages.ChunkDataRequest) {})
   331  
   332  	unittest.RequireReturnsBefore(t, requestHistoryWG.Wait, time.Duration(2)*s.retryInterval, "could not check chunk requests qualification on time")
   333  	unittest.RequireReturnsBefore(t, updateHistoryWG.Wait, s.retryInterval, "could not update chunk request history on time")
   334  	unittest.RequireReturnsBefore(t, notifierWG.Wait, time.Duration(2)*s.retryInterval, "could not notify the handler on time")
   335  	unittest.RequireReturnsBefore(t, conduitWG.Wait, time.Duration(2)*s.retryInterval, "could not request chunks from network")
   336  	unittest.RequireCloseBefore(t, e.Done(), time.Second, "could not stop engine on time")
   337  
   338  	testifymock.AssertExpectationsForObjects(t, s.metrics)
   339  }
   340  
   341  // TestReceivingChunkDataResponseForDuplicateChunkRequests evaluates happy path of receiving a chunk data pack response
   342  // for duplicate chunk data pack requests.
   343  // On receiving the chunk data pack, requester engine should send a chunk data response to the chunk handler for each
   344  // of those pending duplicate chunk data requests.
   345  // Note that by duplicate chunk data requests we mean chunks requests for same chunk ID that belong to
   346  // distinct execution results.
   347  func TestReceivingChunkDataResponseForDuplicateChunkRequests(t *testing.T) {
   348  	s := setupTest()
   349  	e := newRequesterEngine(t, s)
   350  
   351  	resultA, _, _, _ := vertestutils.ExecutionResultForkFixture(t)
   352  
   353  	duplicateChunkID := resultA.Chunks[0].ID()
   354  	responseA := unittest.ChunkDataResponseMsgFixture(duplicateChunkID)
   355  
   356  	requestA := unittest.ChunkDataPackRequestFixture(unittest.WithChunkID(duplicateChunkID))
   357  	requestB := unittest.ChunkDataPackRequestFixture(unittest.WithChunkID(duplicateChunkID))
   358  
   359  	requests := verification.ChunkDataPackRequestList{requestA, requestB}
   360  	originID := unittest.IdentifierFixture()
   361  
   362  	mockPendingRequestsPopAll(t, s.pendingRequests, requests)
   363  	handlerWG := mockChunkDataPackHandler(t, s.handler, requests)
   364  
   365  	s.metrics.On("OnChunkDataPackResponseReceivedFromNetworkByRequester").Return().Once()
   366  	s.metrics.On("OnChunkDataPackSentToFetcher").Return().Twice()
   367  
   368  	err := e.Process(channels.RequestChunks, originID, responseA)
   369  	require.Nil(t, err)
   370  
   371  	unittest.RequireReturnsBefore(t, handlerWG.Wait, time.Second, "could not handle chunk data responses on time")
   372  	testifymock.AssertExpectationsForObjects(t, s.con, s.metrics)
   373  }
   374  
   375  // TestHandleChunkDataPack_DuplicateChunkIDs_Sealed evaluates that on receiving duplicate chunk data requests belonging to a sealed
   376  // block, the requester engine is called chunk handler once for each of those requests notifying it of sealed block.
   377  //
   378  // Note that by duplicate chunk data requests we mean chunks requests for same chunk ID that belong to
   379  // distinct execution results.
   380  func TestHandleChunkDataPack_DuplicateChunkIDs_Sealed(t *testing.T) {
   381  	s := setupTest()
   382  	e := newRequesterEngine(t, s)
   383  
   384  	// mocks the requester pipeline
   385  	sealedHeight := uint64(10)
   386  	vertestutils.MockLastSealedHeight(s.state, sealedHeight)
   387  
   388  	resultA, _, _, _ := vertestutils.ExecutionResultForkFixture(t)
   389  	duplicateChunkID := resultA.Chunks[0].ID()
   390  	requestA := unittest.ChunkDataPackRequestFixture(unittest.WithChunkID(duplicateChunkID), unittest.WithHeight(uint64(sealedHeight-1)))
   391  	requestB := unittest.ChunkDataPackRequestFixture(unittest.WithChunkID(duplicateChunkID), unittest.WithHeight(uint64(sealedHeight-1)))
   392  	requests := verification.ChunkDataPackRequestList{requestA, requestB}
   393  
   394  	// we remove pending request on receiving this response
   395  	s.pendingRequests.On("All").Return(requests.UniqueRequestInfo())
   396  	mockPendingRequestsPopAll(t, s.pendingRequests, requests)
   397  	notifierWG := mockNotifyBlockSealedHandler(t, s.handler, requests)
   398  
   399  	// check data pack request is never tried since its block has been sealed.
   400  	s.metrics.On("SetMaxChunkDataPackAttemptsForNextUnsealedHeightAtRequester", uint64(0)).Return().Once()
   401  
   402  	unittest.RequireCloseBefore(t, e.Ready(), time.Second, "could not start engine on time")
   403  
   404  	unittest.RequireReturnsBefore(t, notifierWG.Wait, time.Duration(2)*s.retryInterval, "could not notify the handler on time")
   405  
   406  	unittest.RequireCloseBefore(t, e.Done(), time.Second, "could not stop engine on time")
   407  
   408  	testifymock.AssertExpectationsForObjects(t, s.metrics)
   409  	// requester does not call publish to disseminate the request for this chunk.
   410  	s.con.AssertNotCalled(t, "Publish")
   411  }
   412  
   413  // TestRequestPendingChunkDataPack evaluates happy path of having a single pending chunk requests.
   414  // The chunk belongs to a non-sealed block.
   415  // On timer interval, the chunk requests should be dispatched to the set of execution nodes agree with the execution
   416  // result the chunk belongs to.
   417  func TestRequestPendingChunkDataPack(t *testing.T) {
   418  	testRequestPendingChunkDataPack(t, 1, 1)   // one request each one attempt
   419  	testRequestPendingChunkDataPack(t, 10, 1)  // 10 requests each one attempt
   420  	testRequestPendingChunkDataPack(t, 10, 10) // 10 requests each 10 attempts
   421  }
   422  
   423  // testRequestPendingChunkDataPack is a test helper that evaluates happy path of having a number of chunk requests pending.
   424  // The test waits enough so that the required number of attempts is made on the chunks.
   425  // The chunks belongs to a non-sealed block.
   426  func testRequestPendingChunkDataPack(t *testing.T, count int, attempts int) {
   427  	s := setupTest()
   428  	e := newRequesterEngine(t, s)
   429  
   430  	// creates 10 chunk request each with 2 agree targets and 3 disagree targets.
   431  	// chunk belongs to a block at heights greater than 5, but the last sealed block is at height 5, so
   432  	// the chunk request should be dispatched.
   433  	agrees := unittest.IdentifierListFixture(2)
   434  	disagrees := unittest.IdentifierListFixture(3)
   435  	requests := unittest.ChunkDataPackRequestListFixture(count,
   436  		unittest.WithHeightGreaterThan(5),
   437  		unittest.WithAgrees(agrees),
   438  		unittest.WithDisagrees(disagrees))
   439  	vertestutils.MockLastSealedHeight(s.state, 5)
   440  	s.pendingRequests.On("All").Return(requests.UniqueRequestInfo())
   441  
   442  	// makes all chunk requests being qualified for dispatch instantly
   443  	requestHistory, updateHistoryWG := mockPendingRequestInfoAndUpdate(t,
   444  		s.pendingRequests,
   445  		requests,
   446  		verification.ChunkDataPackRequestList{},
   447  		verification.ChunkDataPackRequestList{},
   448  		attempts)
   449  
   450  	s.metrics.On("OnChunkDataPackRequestDispatchedInNetworkByRequester").Return().Times(count * attempts)
   451  	s.metrics.On("SetMaxChunkDataPackAttemptsForNextUnsealedHeightAtRequester", testifymock.Anything).Run(func(args testifymock.Arguments) {
   452  		actualAttempts, ok := args[0].(uint64)
   453  		require.True(t, ok)
   454  
   455  		require.LessOrEqual(t, actualAttempts, uint64(attempts))
   456  	}).Return().Times(attempts)
   457  
   458  	unittest.RequireCloseBefore(t, e.Ready(), time.Second, "could not start engine on time")
   459  
   460  	conduitWG := mockConduitForChunkDataPackRequest(t, s.con, requests, attempts, func(*messages.ChunkDataRequest) {})
   461  	unittest.RequireReturnsBefore(t, requestHistory.Wait, time.Duration(2*attempts)*s.retryInterval, "could not check chunk requests qualification on time")
   462  	unittest.RequireReturnsBefore(t, updateHistoryWG.Wait, s.retryInterval, "could not update chunk request history on time")
   463  	unittest.RequireReturnsBefore(t, conduitWG.Wait, time.Duration(2*attempts)*s.retryInterval, "could not request and handle chunks on time")
   464  
   465  	unittest.RequireCloseBefore(t, e.Done(), time.Second, "could not stop engine on time")
   466  	testifymock.AssertExpectationsForObjects(t, s.pendingRequests, s.metrics)
   467  }
   468  
   469  // TestDispatchingRequests_Hybrid evaluates the behavior of requester when it has different request dispatch timelines, i.e.,
   470  // some requests should be dispatched instantly to the network. Some others are old and planned for late dispatch (out of this test timeline),
   471  // and some other should not be dispatched since they no longer are needed (and will be cleaned on next iteration).
   472  //
   473  // The test evaluates that only requests that are instantly planned are getting dispatched to the network.
   474  func TestDispatchingRequests_Hybrid(t *testing.T) {
   475  	s := setupTest()
   476  	e := newRequesterEngine(t, s)
   477  
   478  	// Generates 30 requests, 10 of each type.
   479  	//
   480  	// requests belong to the chunks of
   481  	// a block at heights greater than 5, but the last sealed block is at height 5, so
   482  	// the chunk request should be dispatched.
   483  	agrees := unittest.IdentifierListFixture(2)
   484  	disagrees := unittest.IdentifierListFixture(3)
   485  	vertestutils.MockLastSealedHeight(s.state, 5)
   486  	// models new requests that are just added to the mempool and are ready to dispatch.
   487  	instantQualifiedRequests := unittest.ChunkDataPackRequestListFixture(10,
   488  		unittest.WithHeightGreaterThan(5),
   489  		unittest.WithAgrees(agrees),
   490  		unittest.WithDisagrees(disagrees))
   491  	// models old requests that stayed long in the mempool and are not dispatched anytime soon.
   492  	lateQualifiedRequests := unittest.ChunkDataPackRequestListFixture(10,
   493  		unittest.WithHeightGreaterThan(5),
   494  		unittest.WithAgrees(agrees),
   495  		unittest.WithDisagrees(disagrees))
   496  	// models requests that their chunk data pack arrives during the dispatch processing and hence
   497  	// are no longer needed to dispatch.
   498  	disQualifiedRequests := unittest.ChunkDataPackRequestListFixture(10,
   499  		unittest.WithHeightGreaterThan(5),
   500  		unittest.WithAgrees(agrees),
   501  		unittest.WithDisagrees(disagrees))
   502  
   503  	allRequests := append(instantQualifiedRequests, lateQualifiedRequests...)
   504  	allRequests = append(allRequests, disQualifiedRequests...)
   505  	s.pendingRequests.On("All").Return(allRequests.UniqueRequestInfo())
   506  
   507  	attempts := 10 // waits for 10 iterations of onTimer cycle in requester.
   508  	requestHistoryWG, updateHistoryWG := mockPendingRequestInfoAndUpdate(t,
   509  		s.pendingRequests,
   510  		instantQualifiedRequests,
   511  		lateQualifiedRequests,
   512  		disQualifiedRequests,
   513  		attempts)
   514  
   515  	unittest.RequireCloseBefore(t, e.Ready(), time.Second, "could not start engine on time")
   516  
   517  	// mocks only instantly qualified requests are dispatched in the network.
   518  	conduitWG := mockConduitForChunkDataPackRequest(t, s.con, instantQualifiedRequests, attempts, func(*messages.ChunkDataRequest) {})
   519  	s.metrics.On("OnChunkDataPackRequestDispatchedInNetworkByRequester").Return().Times(len(instantQualifiedRequests) * attempts)
   520  	// each instantly qualified one is requested only once, hence the maximum is updated only once from 0 -> 1, and
   521  	// is kept at 1 during all cycles of this test.
   522  	s.metrics.On("SetMaxChunkDataPackAttemptsForNextUnsealedHeightAtRequester", uint64(1)).Return()
   523  
   524  	unittest.RequireReturnsBefore(t, requestHistoryWG.Wait, time.Duration(2*attempts)*s.retryInterval,
   525  		"could not check chunk requests qualification on time")
   526  	unittest.RequireReturnsBefore(t, updateHistoryWG.Wait, time.Duration(2*attempts)*s.retryInterval,
   527  		"could not update chunk request history on time")
   528  	unittest.RequireReturnsBefore(t, conduitWG.Wait, time.Duration(2*attempts)*s.retryInterval,
   529  		"could not request and handle chunks on time")
   530  	unittest.RequireCloseBefore(t, e.Done(), time.Second, "could not stop engine on time")
   531  
   532  	testifymock.AssertExpectationsForObjects(t, s.pendingRequests, s.metrics)
   533  }
   534  
   535  // toChunkIDs is a test helper that extracts chunk ids from chunk data pack requests.
   536  func toChunkIDs(t *testing.T, requests verification.ChunkDataPackRequestList) flow.IdentifierList {
   537  	var chunkIDs flow.IdentifierList
   538  	for _, request := range requests {
   539  		require.NotContains(t, chunkIDs, request.ChunkID, "duplicate chunk ID found in fixture")
   540  		chunkIDs = append(chunkIDs, request.ChunkID)
   541  	}
   542  	return chunkIDs
   543  }
   544  
   545  // mockConduitForChunkDataPackRequest mocks given conduit for requesting chunk data packs for given chunk IDs.
   546  // Each chunk should be requested exactly `count` many time.
   547  // Upon request, the given request handler is invoked.
   548  // Also, the entire process should not exceed longer than the specified timeout.
   549  func mockConduitForChunkDataPackRequest(t *testing.T,
   550  	con *mocknetwork.Conduit,
   551  	reqList verification.ChunkDataPackRequestList,
   552  	count int,
   553  	requestHandler func(*messages.ChunkDataRequest)) *sync.WaitGroup {
   554  
   555  	// counts number of requests for each chunk data pack
   556  	reqCount := make(map[flow.Identifier]int)
   557  	reqMap := make(map[flow.Identifier]*verification.ChunkDataPackRequest)
   558  	for _, request := range reqList {
   559  		reqCount[request.ChunkID] = 0
   560  		reqMap[request.ChunkID] = request
   561  	}
   562  	wg := &sync.WaitGroup{}
   563  
   564  	// to counter race condition in concurrent invocations of Run
   565  	mutex := &sync.Mutex{}
   566  	wg.Add(count * len(reqList))
   567  
   568  	con.On("Publish", testifymock.Anything, testifymock.Anything, testifymock.Anything).
   569  		Run(func(args testifymock.Arguments) {
   570  			mutex.Lock()
   571  			defer mutex.Unlock()
   572  
   573  			// requested chunk id from network should belong to list of chunk id requests the engine received.
   574  			// also, it should not be repeated below a maximum threshold
   575  			req, ok := args[0].(*messages.ChunkDataRequest)
   576  			require.True(t, ok)
   577  			require.True(t, reqList.ContainsChunkID(req.ChunkID))
   578  			require.LessOrEqual(t, reqCount[req.ChunkID], count)
   579  			reqCount[req.ChunkID]++
   580  
   581  			// requested chunk ids should only be passed to agreed execution nodes
   582  			target1, ok := args[1].(flow.Identifier)
   583  			require.True(t, ok)
   584  			require.Contains(t, reqMap[req.ChunkID].Agrees, target1)
   585  
   586  			target2, ok := args[2].(flow.Identifier)
   587  			require.True(t, ok)
   588  			require.Contains(t, reqMap[req.ChunkID].Agrees, target2)
   589  
   590  			go func() {
   591  				requestHandler(req)
   592  				wg.Done()
   593  			}()
   594  
   595  		}).Return(nil)
   596  
   597  	return wg
   598  }
   599  
   600  // mockChunkDataPackHandler mocks chunk data pack handler for receiving a set of chunk responses.
   601  // It evaluates that, each pair of (chunkIndex, resultID) should be passed exactly once.
   602  func mockChunkDataPackHandler(t *testing.T, handler *mockfetcher.ChunkDataPackHandler, requests verification.ChunkDataPackRequestList) *sync.WaitGroup {
   603  	handledLocators := make(map[flow.Identifier]struct{})
   604  
   605  	wg := sync.WaitGroup{}
   606  	wg.Add(len(requests))
   607  	handler.On("HandleChunkDataPack", testifymock.Anything, testifymock.Anything).
   608  		Run(func(args testifymock.Arguments) {
   609  			_, ok := args[0].(flow.Identifier)
   610  			require.True(t, ok)
   611  			response, ok := args[1].(*verification.ChunkDataPackResponse)
   612  			require.True(t, ok)
   613  
   614  			// we should have already requested this chunk data pack.
   615  			require.True(t, requests.ContainsLocator(response.ResultID, response.Index))
   616  			require.True(t, requests.ContainsChunkID(response.Cdp.ChunkID))
   617  
   618  			// invocation should be distinct per chunk ID
   619  			locatorID := chunks.ChunkLocatorID(response.ResultID, response.Index)
   620  			_, ok = handledLocators[locatorID]
   621  			require.False(t, ok)
   622  
   623  			handledLocators[locatorID] = struct{}{}
   624  
   625  			wg.Done()
   626  		}).Return()
   627  
   628  	return &wg
   629  }
   630  
   631  // mockChunkDataPackHandler mocks chunk data pack handler for being notified that a set of chunk IDs are sealed.
   632  // It evaluates that, each chunk ID should be notified only once.
   633  func mockNotifyBlockSealedHandler(t *testing.T, handler *mockfetcher.ChunkDataPackHandler, requests verification.ChunkDataPackRequestList) *sync.WaitGroup {
   634  
   635  	wg := &sync.WaitGroup{}
   636  	wg.Add(len(requests))
   637  	// maps keep track of distinct invocations per chunk ID
   638  	seen := make(map[flow.Identifier]struct{})
   639  	handler.On("NotifyChunkDataPackSealed", testifymock.Anything, testifymock.Anything).
   640  		Run(func(args testifymock.Arguments) {
   641  			chunkIndex, ok := args[0].(uint64)
   642  			require.True(t, ok)
   643  			resultID, ok := args[1].(flow.Identifier)
   644  			require.True(t, ok)
   645  
   646  			// we should have already requested this chunk data pack, and collection ID should be the same.
   647  			require.True(t, requests.ContainsLocator(resultID, chunkIndex))
   648  
   649  			// invocation should be distinct per chunk ID
   650  			locatorID := chunks.ChunkLocatorID(resultID, chunkIndex)
   651  			_, ok = seen[locatorID]
   652  			require.False(t, ok)
   653  			seen[locatorID] = struct{}{}
   654  
   655  			wg.Done()
   656  		}).Return()
   657  
   658  	return wg
   659  }
   660  
   661  // mockPendingRequestsPopAll mocks chunk requests mempool for being queried for returning all requests associated with a
   662  // chunk ID only once.
   663  func mockPendingRequestsPopAll(t *testing.T, pendingRequests *mempool.ChunkRequests, requests verification.ChunkDataPackRequestList) {
   664  	// maps keep track of distinct invocations per chunk ID
   665  	seen := make(map[flow.Identifier]struct{})
   666  
   667  	pendingRequests.On("PopAll", testifymock.Anything).
   668  		Return(
   669  			func(chunkID flow.Identifier) chunks.LocatorMap {
   670  				locators := make(chunks.LocatorMap)
   671  
   672  				// chunk ID must not be seen
   673  				_, ok := seen[chunkID]
   674  				require.False(t, ok)
   675  
   676  				for _, request := range requests {
   677  					if request.ChunkID == chunkID {
   678  						locator := request.Locator
   679  						locators[locator.ID()] = &locator
   680  					}
   681  				}
   682  
   683  				seen[chunkID] = struct{}{}
   684  				return locators
   685  			},
   686  			func(chunkID flow.Identifier) bool {
   687  				for _, request := range requests {
   688  					if request.ChunkID == chunkID {
   689  						return true
   690  					}
   691  				}
   692  
   693  				return false
   694  			},
   695  		)
   696  }
   697  
   698  // mockPendingRequestInfoAndUpdate mocks pending requests mempool regarding three sets of chunk IDs: the instant, late, and disqualified ones.
   699  // The chunk IDs in the instantly qualified requests will be instantly qualified for dispatching in the networking layer.
   700  // The chunk IDs in the late qualified requests will be postponed to a very later time for dispatching. The postponed time is set so long
   701  // that they literally never get the chance to dispatch within the test time, e.g., 1 hour.
   702  // The chunk IDs in the disqualified requests do not dispatch at all.
   703  //
   704  // The disqualified ones represent the set of chunk requests that are cleaned from memory during the on timer iteration of the requester
   705  // engine, and are no longer needed.
   706  func mockPendingRequestInfoAndUpdate(t *testing.T,
   707  	pendingRequests *mempool.ChunkRequests,
   708  	instantQualifiedReqs verification.ChunkDataPackRequestList,
   709  	lateQualifiedReqs verification.ChunkDataPackRequestList,
   710  	disQualifiedReqs verification.ChunkDataPackRequestList,
   711  	attempts int) (*sync.WaitGroup, *sync.WaitGroup) {
   712  
   713  	historyWG := &sync.WaitGroup{}
   714  
   715  	// for purpose of test and due to having a mocked mempool, we assume disqualified requests reside on the
   716  	// mempool, so their qualification is getting checked on each attempt iteration (and rejected).
   717  	totalRequestHistory := attempts * (len(instantQualifiedReqs) + len(lateQualifiedReqs) + len(disQualifiedReqs))
   718  	historyWG.Add(totalRequestHistory)
   719  
   720  	pendingRequests.On("RequestHistory", testifymock.Anything).
   721  		Run(func(args testifymock.Arguments) {
   722  			// type assertion of input.
   723  			chunkID, ok := args[0].(flow.Identifier)
   724  			require.True(t, ok)
   725  
   726  			// chunk ID should be one of the expected ones.
   727  			require.True(t,
   728  				instantQualifiedReqs.ContainsChunkID(chunkID) ||
   729  					lateQualifiedReqs.ContainsChunkID(chunkID) ||
   730  					disQualifiedReqs.ContainsChunkID(chunkID))
   731  
   732  			historyWG.Done()
   733  
   734  		}).Return(
   735  		// number of attempts
   736  		func(chunkID flow.Identifier) uint64 {
   737  			if instantQualifiedReqs.ContainsChunkID(chunkID) || lateQualifiedReqs.ContainsChunkID(chunkID) {
   738  				return uint64(1)
   739  			}
   740  
   741  			return uint64(0)
   742  
   743  		}, // last tried timestamp
   744  		func(chunkID flow.Identifier) time.Time {
   745  			if instantQualifiedReqs.ContainsChunkID(chunkID) {
   746  				// mocks last tried long enough so they instantly get qualified.
   747  				return time.Now().Add(-1 * time.Hour)
   748  			}
   749  
   750  			if lateQualifiedReqs.ContainsChunkID(chunkID) {
   751  				return time.Now()
   752  			}
   753  
   754  			return time.Time{}
   755  		}, // retry after duration
   756  		func(chunkID flow.Identifier) time.Duration {
   757  			if instantQualifiedReqs.ContainsChunkID(chunkID) {
   758  				// mocks retry after very short so they instantly get qualified.
   759  				return 1 * time.Millisecond
   760  			}
   761  
   762  			if lateQualifiedReqs.ContainsChunkID(chunkID) {
   763  				// mocks retry after long so they never qualify soon.
   764  				return time.Hour
   765  			}
   766  
   767  			return 0
   768  
   769  		}, // request info existence
   770  		func(chunkID flow.Identifier) bool {
   771  			if instantQualifiedReqs.ContainsChunkID(chunkID) || lateQualifiedReqs.ContainsChunkID(chunkID) {
   772  				return true
   773  			}
   774  
   775  			return false
   776  		},
   777  	)
   778  
   779  	updateWG := &sync.WaitGroup{}
   780  	updateWG.Add(len(instantQualifiedReqs) * attempts)
   781  	pendingRequests.On("UpdateRequestHistory", testifymock.Anything, testifymock.Anything).
   782  		Run(func(args testifymock.Arguments) {
   783  			// type assertion of inputs.
   784  			chunkID, ok := args[0].(flow.Identifier)
   785  			require.True(t, ok)
   786  
   787  			_, ok = args[1].(flowmempool.ChunkRequestHistoryUpdaterFunc)
   788  			require.True(t, ok)
   789  
   790  			// checks only instantly qualified chunk requests should reach to this step,
   791  			// i.e., invocation of UpdateRequestHistory
   792  			require.True(t, instantQualifiedReqs.ContainsChunkID(chunkID))
   793  			require.False(t, lateQualifiedReqs.ContainsChunkID(chunkID))
   794  			require.False(t, disQualifiedReqs.ContainsChunkID(chunkID))
   795  
   796  			updateWG.Done()
   797  
   798  		}).
   799  		Return(uint64(1), time.Now(), 1*time.Millisecond, true)
   800  
   801  	return historyWG, updateWG
   802  }