github.com/koko1123/flow-go-1@v0.29.6/module/state_synchronization/requester/execution_data_requester_test.go (about)

     1  package requester_test
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/rand"
     7  	"os"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/dgraph-io/badger/v3"
    13  	"github.com/ipfs/go-datastore"
    14  	dssync "github.com/ipfs/go-datastore/sync"
    15  	"github.com/rs/zerolog"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/mock"
    18  	"github.com/stretchr/testify/require"
    19  	"github.com/stretchr/testify/suite"
    20  
    21  	"github.com/koko1123/flow-go-1/consensus/hotstuff/model"
    22  	"github.com/koko1123/flow-go-1/consensus/hotstuff/notifications/pubsub"
    23  	"github.com/koko1123/flow-go-1/model/flow"
    24  	"github.com/koko1123/flow-go-1/module"
    25  	"github.com/koko1123/flow-go-1/module/blobs"
    26  	"github.com/koko1123/flow-go-1/module/executiondatasync/execution_data"
    27  	exedatamock "github.com/koko1123/flow-go-1/module/executiondatasync/execution_data/mock"
    28  	"github.com/koko1123/flow-go-1/module/irrecoverable"
    29  	"github.com/koko1123/flow-go-1/module/metrics"
    30  	"github.com/koko1123/flow-go-1/module/state_synchronization"
    31  	"github.com/koko1123/flow-go-1/module/state_synchronization/requester"
    32  	synctest "github.com/koko1123/flow-go-1/module/state_synchronization/requester/unittest"
    33  	"github.com/koko1123/flow-go-1/state/protocol"
    34  	statemock "github.com/koko1123/flow-go-1/state/protocol/mock"
    35  	bstorage "github.com/koko1123/flow-go-1/storage/badger"
    36  	"github.com/koko1123/flow-go-1/utils/unittest"
    37  )
    38  
    39  type ExecutionDataRequesterSuite struct {
    40  	suite.Suite
    41  
    42  	blobstore  blobs.Blobstore
    43  	datastore  datastore.Batching
    44  	db         *badger.DB
    45  	downloader *exedatamock.Downloader
    46  
    47  	run edTestRun
    48  
    49  	mockSnapshot *mockSnapshot
    50  }
    51  
    52  func TestExecutionDataRequesterSuite(t *testing.T) {
    53  	t.Parallel()
    54  	rand.Seed(time.Now().UnixMilli())
    55  	suite.Run(t, new(ExecutionDataRequesterSuite))
    56  }
    57  
    58  func (suite *ExecutionDataRequesterSuite) SetupTest() {
    59  	suite.datastore = dssync.MutexWrap(datastore.NewMapDatastore())
    60  	suite.blobstore = blobs.NewBlobstore(suite.datastore)
    61  
    62  	suite.run = edTestRun{
    63  		"",
    64  		100,
    65  		func(_ int) map[uint64]testExecutionDataCallback {
    66  			return map[uint64]testExecutionDataCallback{}
    67  		},
    68  	}
    69  }
    70  
    71  type testExecutionDataServiceEntry struct {
    72  	// When set, the response from this call back will be returned for any calls to Get
    73  	// Note: this callback is called twice by mockery, once for the execution data and once for the error
    74  	fn testExecutionDataCallback
    75  	// When set (and fn is unset), this error will be returned for any calls to Get for this ED
    76  	Err error
    77  	// Otherwise, the execution data will be returned directly with no error
    78  	ExecutionData *execution_data.BlockExecutionData
    79  }
    80  
    81  type specialBlockGenerator func(int) map[uint64]testExecutionDataCallback
    82  type edTestRun struct {
    83  	name          string
    84  	blockCount    int
    85  	specialBlocks specialBlockGenerator
    86  }
    87  
    88  type testExecutionDataCallback func(*execution_data.BlockExecutionData) (*execution_data.BlockExecutionData, error)
    89  
    90  func mockDownloader(edStore map[flow.Identifier]*testExecutionDataServiceEntry) *exedatamock.Downloader {
    91  	downloader := new(exedatamock.Downloader)
    92  
    93  	get := func(id flow.Identifier) (*execution_data.BlockExecutionData, error) {
    94  		ed, has := edStore[id]
    95  
    96  		// return not found
    97  		if !has {
    98  			return nil, execution_data.NewBlobNotFoundError(flow.IdToCid(id))
    99  		}
   100  
   101  		// use a callback. this is useful for injecting a pause or custom error behavior
   102  		if ed.fn != nil {
   103  			return ed.fn(ed.ExecutionData)
   104  		}
   105  
   106  		// return a custom error
   107  		if ed.Err != nil {
   108  			return nil, ed.Err
   109  		}
   110  
   111  		// return the specific execution data
   112  		return ed.ExecutionData, nil
   113  	}
   114  
   115  	downloader.On("Download", mock.Anything, mock.AnythingOfType("flow.Identifier")).
   116  		Return(
   117  			func(ctx context.Context, id flow.Identifier) *execution_data.BlockExecutionData {
   118  				ed, _ := get(id)
   119  				return ed
   120  			},
   121  			func(ctx context.Context, id flow.Identifier) error {
   122  				_, err := get(id)
   123  				return err
   124  			},
   125  		).
   126  		Maybe() // Maybe() needed to get call count
   127  
   128  	noop := module.NoopReadyDoneAware{}
   129  	downloader.On("Ready").
   130  		Return(func() <-chan struct{} { return noop.Ready() }).
   131  		Maybe() // Maybe() needed to get call count
   132  
   133  	return downloader
   134  }
   135  
   136  func (suite *ExecutionDataRequesterSuite) mockProtocolState(blocksByHeight map[uint64]*flow.Block) *statemock.State {
   137  	state := new(statemock.State)
   138  
   139  	suite.mockSnapshot = new(mockSnapshot)
   140  	suite.mockSnapshot.set(blocksByHeight[0].Header, nil) // genesis block
   141  
   142  	state.On("Sealed").Return(suite.mockSnapshot).Maybe()
   143  	return state
   144  }
   145  
   146  // TestRequesterProcessesBlocks tests that the requester processes all blocks and sends notifications
   147  // in order.
   148  func (suite *ExecutionDataRequesterSuite) TestRequesterProcessesBlocks() {
   149  
   150  	tests := []edTestRun{
   151  		// Test that blocks are processed in order
   152  		{
   153  			"happy path",
   154  			100,
   155  			func(_ int) map[uint64]testExecutionDataCallback {
   156  				return map[uint64]testExecutionDataCallback{}
   157  			},
   158  		},
   159  		// Tests that blocks that are missed are properly retried and notifications are received in order
   160  		{
   161  			"requests blocks with some missed",
   162  			100,
   163  			generateBlocksWithSomeMissed,
   164  		},
   165  		// Tests that blocks that are missed are properly retried and backfilled
   166  		{
   167  			"requests blocks with some delayed",
   168  			100,
   169  			generateBlocksWithRandomDelays,
   170  		},
   171  	}
   172  
   173  	for _, run := range tests {
   174  		suite.Run(run.name, func() {
   175  			unittest.RunWithBadgerDB(suite.T(), func(db *badger.DB) {
   176  				suite.db = db
   177  
   178  				suite.datastore = dssync.MutexWrap(datastore.NewMapDatastore())
   179  				suite.blobstore = blobs.NewBlobstore(suite.datastore)
   180  
   181  				testData := suite.generateTestData(run.blockCount, run.specialBlocks(run.blockCount))
   182  				edr, fd := suite.prepareRequesterTest(testData)
   183  				fetchedExecutionData := suite.runRequesterTest(edr, fd, testData)
   184  
   185  				verifyFetchedExecutionData(suite.T(), fetchedExecutionData, testData)
   186  
   187  				suite.T().Log("Shutting down test")
   188  			})
   189  		})
   190  	}
   191  }
   192  
   193  // TestRequesterResumesAfterRestart tests that the requester will pick up where it left off after a
   194  // restart, without skipping any blocks
   195  func (suite *ExecutionDataRequesterSuite) TestRequesterResumesAfterRestart() {
   196  	suite.datastore = dssync.MutexWrap(datastore.NewMapDatastore())
   197  	suite.blobstore = blobs.NewBlobstore(suite.datastore)
   198  
   199  	testData := suite.generateTestData(suite.run.blockCount, suite.run.specialBlocks(suite.run.blockCount))
   200  
   201  	test := func(stopHeight, resumeHeight uint64) {
   202  		testData.fetchedExecutionData = nil
   203  
   204  		unittest.RunWithBadgerDB(suite.T(), func(db *badger.DB) {
   205  			suite.db = db
   206  
   207  			// Process half of the blocks
   208  			edr, fd := suite.prepareRequesterTest(testData)
   209  			testData.stopHeight = stopHeight
   210  			testData.resumeHeight = 0
   211  			testData.fetchedExecutionData = suite.runRequesterTest(edr, fd, testData)
   212  
   213  			// Stand up a new component using the same datastore, and make sure all remaining
   214  			// blocks are processed
   215  			edr, fd = suite.prepareRequesterTest(testData)
   216  			testData.stopHeight = 0
   217  			testData.resumeHeight = resumeHeight
   218  			fetchedExecutionData := suite.runRequesterTest(edr, fd, testData)
   219  
   220  			verifyFetchedExecutionData(suite.T(), fetchedExecutionData, testData)
   221  
   222  			suite.T().Log("Shutting down test")
   223  		})
   224  	}
   225  
   226  	suite.Run("requester resumes processing with no gap", func() {
   227  		stopHeight := testData.startHeight + uint64(suite.run.blockCount)/2
   228  		resumeHeight := stopHeight + 1
   229  		test(stopHeight, resumeHeight)
   230  	})
   231  
   232  	suite.Run("requester resumes processing with gap", func() {
   233  		stopHeight := testData.startHeight + uint64(suite.run.blockCount)/2
   234  		resumeHeight := testData.endHeight
   235  		test(stopHeight, resumeHeight)
   236  	})
   237  }
   238  
   239  // TestRequesterCatchesUp tests that the requester processes all heights when it starts with a
   240  // backlog of sealed blocks.
   241  func (suite *ExecutionDataRequesterSuite) TestRequesterCatchesUp() {
   242  	unittest.RunWithBadgerDB(suite.T(), func(db *badger.DB) {
   243  		suite.db = db
   244  
   245  		suite.datastore = dssync.MutexWrap(datastore.NewMapDatastore())
   246  		suite.blobstore = blobs.NewBlobstore(suite.datastore)
   247  
   248  		testData := suite.generateTestData(suite.run.blockCount, suite.run.specialBlocks(suite.run.blockCount))
   249  
   250  		// start processing with all seals available
   251  		edr, fd := suite.prepareRequesterTest(testData)
   252  		testData.resumeHeight = testData.endHeight
   253  		fetchedExecutionData := suite.runRequesterTest(edr, fd, testData)
   254  
   255  		verifyFetchedExecutionData(suite.T(), fetchedExecutionData, testData)
   256  
   257  		suite.T().Log("Shutting down test")
   258  	})
   259  }
   260  
   261  // TestRequesterPausesAndResumes tests that the requester pauses when it downloads maxSearchAhead
   262  // blocks beyond the last processed block, and resumes when it catches up.
   263  func (suite *ExecutionDataRequesterSuite) TestRequesterPausesAndResumes() {
   264  	unittest.RunWithBadgerDB(suite.T(), func(db *badger.DB) {
   265  		suite.db = db
   266  
   267  		pauseHeight := uint64(10)
   268  		maxSearchAhead := uint64(5)
   269  
   270  		// Downloads will succeed immediately for all blocks except pauseHeight, which will hang
   271  		// until the resume() is called.
   272  		generate, resume := generatePauseResume(pauseHeight)
   273  
   274  		testData := suite.generateTestData(suite.run.blockCount, generate(suite.run.blockCount))
   275  		testData.maxSearchAhead = maxSearchAhead
   276  		testData.waitTimeout = time.Second * 10
   277  
   278  		// calculate the expected number of blocks that should be downloaded before resuming
   279  		expectedDownloads := maxSearchAhead + (pauseHeight-1)*2
   280  
   281  		edr, fd := suite.prepareRequesterTest(testData)
   282  		fetchedExecutionData := suite.runRequesterTestPauseResume(edr, fd, testData, int(expectedDownloads), resume)
   283  
   284  		verifyFetchedExecutionData(suite.T(), fetchedExecutionData, testData)
   285  
   286  		suite.T().Log("Shutting down test")
   287  	})
   288  }
   289  
   290  // TestRequesterHalts tests that the requester handles halting correctly when it encounters an
   291  // invalid block
   292  func (suite *ExecutionDataRequesterSuite) TestRequesterHalts() {
   293  	unittest.RunWithBadgerDB(suite.T(), func(db *badger.DB) {
   294  		suite.db = db
   295  
   296  		suite.run.blockCount = 10
   297  		suite.datastore = dssync.MutexWrap(datastore.NewMapDatastore())
   298  		suite.blobstore = blobs.NewBlobstore(suite.datastore)
   299  
   300  		// generate a block that will return a malformed blob error. causing the requester to halt
   301  		generate, expectedErr := generateBlocksWithHaltingError(suite.run.blockCount)
   302  		testData := suite.generateTestData(suite.run.blockCount, generate(suite.run.blockCount))
   303  
   304  		// start processing with all seals available
   305  		edr, finalizationDistributor := suite.prepareRequesterTest(testData)
   306  		testData.resumeHeight = testData.endHeight
   307  		testData.expectedIrrecoverable = expectedErr
   308  		fetchedExecutionData := suite.runRequesterTestHalts(edr, finalizationDistributor, testData)
   309  		assert.Less(suite.T(), len(fetchedExecutionData), testData.sealedCount)
   310  
   311  		suite.T().Log("Shutting down test")
   312  	})
   313  }
   314  
   315  func generateBlocksWithSomeMissed(blockCount int) map[uint64]testExecutionDataCallback {
   316  	missing := map[uint64]testExecutionDataCallback{}
   317  
   318  	// every 5th block fails to download n times before succeeding
   319  	for i := uint64(0); i < uint64(blockCount); i++ {
   320  		if i%5 > 0 {
   321  			continue
   322  		}
   323  
   324  		failures := rand.Intn(3) + 1
   325  		attempts := 0
   326  		missing[i] = func(ed *execution_data.BlockExecutionData) (*execution_data.BlockExecutionData, error) {
   327  			if attempts < failures*2 { // this func is run twice for every attempt by the mock (once for ExecutionData one for errors)
   328  				attempts++
   329  				// This should fail the first n fetch attempts
   330  				time.Sleep(time.Duration(rand.Intn(25)) * time.Millisecond)
   331  				return nil, &execution_data.BlobNotFoundError{}
   332  			}
   333  
   334  			return ed, nil
   335  		}
   336  	}
   337  
   338  	return missing
   339  }
   340  
   341  func generateBlocksWithRandomDelays(blockCount int) map[uint64]testExecutionDataCallback {
   342  	// delay every third block by a random amount
   343  	delays := map[uint64]testExecutionDataCallback{}
   344  	for i := uint64(0); i < uint64(blockCount); i++ {
   345  		if i%5 > 0 {
   346  			continue
   347  		}
   348  
   349  		delays[i] = func(ed *execution_data.BlockExecutionData) (*execution_data.BlockExecutionData, error) {
   350  			time.Sleep(time.Duration(rand.Intn(25)) * time.Millisecond)
   351  			return ed, nil
   352  		}
   353  	}
   354  
   355  	return delays
   356  }
   357  
   358  func generateBlocksWithHaltingError(blockCount int) (specialBlockGenerator, error) {
   359  	// return a MalformedDataError on the second to last block
   360  	height := uint64(blockCount - 5)
   361  	err := fmt.Errorf("halting error: %w", &execution_data.MalformedDataError{})
   362  
   363  	generate := func(int) map[uint64]testExecutionDataCallback {
   364  		return map[uint64]testExecutionDataCallback{
   365  			height: func(ed *execution_data.BlockExecutionData) (*execution_data.BlockExecutionData, error) {
   366  				return nil, err
   367  			},
   368  		}
   369  	}
   370  	return generate, err
   371  }
   372  
   373  func generatePauseResume(pauseHeight uint64) (specialBlockGenerator, func()) {
   374  	pause := make(chan struct{})
   375  
   376  	blocks := map[uint64]testExecutionDataCallback{}
   377  	blocks[pauseHeight] = func(ed *execution_data.BlockExecutionData) (*execution_data.BlockExecutionData, error) {
   378  		<-pause
   379  		return ed, nil
   380  	}
   381  
   382  	generate := func(int) map[uint64]testExecutionDataCallback { return blocks }
   383  	resume := func() { close(pause) }
   384  
   385  	return generate, resume
   386  }
   387  
   388  func (suite *ExecutionDataRequesterSuite) prepareRequesterTest(cfg *fetchTestRun) (state_synchronization.ExecutionDataRequester, *pubsub.FinalizationDistributor) {
   389  	headers := synctest.MockBlockHeaderStorage(
   390  		synctest.WithByID(cfg.blocksByID),
   391  		synctest.WithByHeight(cfg.blocksByHeight),
   392  	)
   393  	results := synctest.MockResultsStorage(
   394  		synctest.WithResultByID(cfg.resultsByID),
   395  	)
   396  	seals := synctest.MockSealsStorage(
   397  		synctest.WithSealsByBlockID(cfg.sealsByBlockID),
   398  	)
   399  	state := suite.mockProtocolState(cfg.blocksByHeight)
   400  
   401  	suite.downloader = mockDownloader(cfg.executionDataEntries)
   402  
   403  	finalizationDistributor := pubsub.NewFinalizationDistributor()
   404  	processedHeight := bstorage.NewConsumerProgress(suite.db, module.ConsumeProgressExecutionDataRequesterBlockHeight)
   405  	processedNotification := bstorage.NewConsumerProgress(suite.db, module.ConsumeProgressExecutionDataRequesterNotification)
   406  
   407  	edr := requester.New(
   408  		zerolog.New(os.Stdout).With().Timestamp().Logger(),
   409  		metrics.NewNoopCollector(),
   410  		suite.downloader,
   411  		processedHeight,
   412  		processedNotification,
   413  		state,
   414  		headers,
   415  		results,
   416  		seals,
   417  		requester.ExecutionDataConfig{
   418  			InitialBlockHeight: cfg.startHeight - 1,
   419  			MaxSearchAhead:     cfg.maxSearchAhead,
   420  			FetchTimeout:       cfg.fetchTimeout,
   421  			RetryDelay:         cfg.retryDelay,
   422  			MaxRetryDelay:      cfg.maxRetryDelay,
   423  		},
   424  	)
   425  
   426  	finalizationDistributor.AddOnBlockFinalizedConsumer(edr.OnBlockFinalized)
   427  
   428  	return edr, finalizationDistributor
   429  }
   430  
   431  func (suite *ExecutionDataRequesterSuite) runRequesterTestHalts(edr state_synchronization.ExecutionDataRequester, finalizationDistributor *pubsub.FinalizationDistributor, cfg *fetchTestRun) receivedExecutionData {
   432  	// make sure test helper goroutines are cleaned up
   433  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   434  	defer cancel()
   435  
   436  	signalerCtx := irrecoverable.NewMockSignalerContext(suite.T(), ctx)
   437  
   438  	testDone := make(chan struct{})
   439  	fetchedExecutionData := cfg.FetchedExecutionData()
   440  
   441  	// collect all execution data notifications
   442  	edr.AddOnExecutionDataFetchedConsumer(suite.consumeExecutionDataNotifications(cfg, func() { close(testDone) }, fetchedExecutionData))
   443  
   444  	edr.Start(signalerCtx)
   445  	unittest.RequireCloseBefore(suite.T(), edr.Ready(), cfg.waitTimeout, "timed out waiting for requester to be ready")
   446  
   447  	// Send blocks through finalizationDistributor
   448  	suite.finalizeBlocks(cfg, finalizationDistributor)
   449  
   450  	// testDone should never close because the requester paused
   451  	unittest.RequireNeverClosedWithin(suite.T(), testDone, 100*time.Millisecond, "finished sending notifications unexpectedly")
   452  	suite.T().Log("All notifications received")
   453  
   454  	cancel()
   455  	unittest.RequireCloseBefore(suite.T(), edr.Done(), cfg.waitTimeout, "timed out waiting for requester to shutdown")
   456  
   457  	return fetchedExecutionData
   458  }
   459  
   460  func (suite *ExecutionDataRequesterSuite) runRequesterTestPauseResume(edr state_synchronization.ExecutionDataRequester, finalizationDistributor *pubsub.FinalizationDistributor, cfg *fetchTestRun, expectedDownloads int, resume func()) receivedExecutionData {
   461  	// make sure test helper goroutines are cleaned up
   462  	ctx, cancel := context.WithCancel(context.Background())
   463  	signalerCtx := irrecoverable.NewMockSignalerContext(suite.T(), ctx)
   464  
   465  	testDone := make(chan struct{})
   466  	fetchedExecutionData := cfg.FetchedExecutionData()
   467  
   468  	// collect all execution data notifications
   469  	edr.AddOnExecutionDataFetchedConsumer(suite.consumeExecutionDataNotifications(cfg, func() { close(testDone) }, fetchedExecutionData))
   470  
   471  	edr.Start(signalerCtx)
   472  	unittest.RequireCloseBefore(suite.T(), edr.Ready(), cfg.waitTimeout, "timed out waiting for requester to be ready")
   473  
   474  	// Send all blocks through finalizationDistributor
   475  	suite.finalizeBlocks(cfg, finalizationDistributor)
   476  
   477  	// requester should pause downloads until resume is called, so testDone should not be closed
   478  	unittest.RequireNeverClosedWithin(suite.T(), testDone, 500*time.Millisecond, "finished unexpectedly")
   479  
   480  	// confirm the expected number of downloads were attempted
   481  	suite.downloader.AssertNumberOfCalls(suite.T(), "Download", expectedDownloads)
   482  
   483  	suite.T().Log("Resuming")
   484  	resume()
   485  
   486  	// Pause until we've received all of the expected notifications
   487  	unittest.RequireCloseBefore(suite.T(), testDone, cfg.waitTimeout, "timed out waiting for notifications")
   488  	suite.T().Log("All notifications received")
   489  
   490  	cancel()
   491  	unittest.RequireCloseBefore(suite.T(), edr.Done(), cfg.waitTimeout, "timed out waiting for requester to shutdown")
   492  
   493  	return fetchedExecutionData
   494  }
   495  
   496  func (suite *ExecutionDataRequesterSuite) runRequesterTest(edr state_synchronization.ExecutionDataRequester, finalizationDistributor *pubsub.FinalizationDistributor, cfg *fetchTestRun) receivedExecutionData {
   497  	// make sure test helper goroutines are cleaned up
   498  	ctx, cancel := context.WithCancel(context.Background())
   499  	signalerCtx := irrecoverable.NewMockSignalerContext(suite.T(), ctx)
   500  
   501  	// wait for all notifications
   502  	testDone := make(chan struct{})
   503  
   504  	fetchedExecutionData := cfg.FetchedExecutionData()
   505  
   506  	// collect all execution data notifications
   507  	edr.AddOnExecutionDataFetchedConsumer(suite.consumeExecutionDataNotifications(cfg, func() { close(testDone) }, fetchedExecutionData))
   508  
   509  	edr.Start(signalerCtx)
   510  	unittest.RequireCloseBefore(suite.T(), edr.Ready(), cfg.waitTimeout, "timed out waiting for requester to be ready")
   511  
   512  	// Send blocks through finalizationDistributor
   513  	suite.finalizeBlocks(cfg, finalizationDistributor)
   514  
   515  	// Pause until we've received all of the expected notifications
   516  	unittest.RequireCloseBefore(suite.T(), testDone, cfg.waitTimeout, "timed out waiting for notifications")
   517  	suite.T().Log("All notifications received")
   518  
   519  	cancel()
   520  	unittest.RequireCloseBefore(suite.T(), edr.Done(), cfg.waitTimeout, "timed out waiting for requester to shutdown")
   521  
   522  	return fetchedExecutionData
   523  }
   524  
   525  func (suite *ExecutionDataRequesterSuite) consumeExecutionDataNotifications(cfg *fetchTestRun, done func(), fetchedExecutionData map[flow.Identifier]*execution_data.BlockExecutionData) func(ed *execution_data.BlockExecutionData) {
   526  	return func(ed *execution_data.BlockExecutionData) {
   527  		if _, has := fetchedExecutionData[ed.BlockID]; has {
   528  			suite.T().Errorf("duplicate execution data for block %s", ed.BlockID)
   529  			return
   530  		}
   531  
   532  		fetchedExecutionData[ed.BlockID] = ed
   533  		suite.T().Logf("notified of execution data for block %v height %d (%d/%d)", ed.BlockID, cfg.blocksByID[ed.BlockID].Header.Height, len(fetchedExecutionData), cfg.sealedCount)
   534  
   535  		if cfg.IsLastSeal(ed.BlockID) {
   536  			done()
   537  		}
   538  	}
   539  }
   540  
   541  func (suite *ExecutionDataRequesterSuite) finalizeBlocks(cfg *fetchTestRun, finalizationDistributor *pubsub.FinalizationDistributor) {
   542  	for i := cfg.StartHeight(); i <= cfg.endHeight; i++ {
   543  		b := cfg.blocksByHeight[i]
   544  
   545  		suite.T().Log(">>>> Finalizing block", b.ID(), b.Header.Height)
   546  
   547  		if len(b.Payload.Seals) > 0 {
   548  			seal := b.Payload.Seals[0]
   549  			sealedHeader := cfg.blocksByID[seal.BlockID].Header
   550  
   551  			suite.mockSnapshot.set(sealedHeader, nil)
   552  			suite.T().Log(">>>> Sealing block", sealedHeader.ID(), sealedHeader.Height)
   553  		}
   554  
   555  		finalizationDistributor.OnFinalizedBlock(&model.Block{}) // actual block is unused
   556  
   557  		if cfg.stopHeight == i {
   558  			break
   559  		}
   560  	}
   561  }
   562  
   563  type receivedExecutionData map[flow.Identifier]*execution_data.BlockExecutionData
   564  type fetchTestRun struct {
   565  	sealedCount              int
   566  	startHeight              uint64
   567  	endHeight                uint64
   568  	blocksByHeight           map[uint64]*flow.Block
   569  	blocksByID               map[flow.Identifier]*flow.Block
   570  	resultsByID              map[flow.Identifier]*flow.ExecutionResult
   571  	resultsByBlockID         map[flow.Identifier]*flow.ExecutionResult
   572  	sealsByBlockID           map[flow.Identifier]*flow.Seal
   573  	executionDataByID        map[flow.Identifier]*execution_data.BlockExecutionData
   574  	executionDataEntries     map[flow.Identifier]*testExecutionDataServiceEntry
   575  	executionDataIDByBlockID map[flow.Identifier]flow.Identifier
   576  	expectedIrrecoverable    error
   577  
   578  	stopHeight           uint64
   579  	resumeHeight         uint64
   580  	fetchedExecutionData map[flow.Identifier]*execution_data.BlockExecutionData
   581  	waitTimeout          time.Duration
   582  
   583  	maxSearchAhead uint64
   584  	fetchTimeout   time.Duration
   585  	retryDelay     time.Duration
   586  	maxRetryDelay  time.Duration
   587  }
   588  
   589  func (r *fetchTestRun) StartHeight() uint64 {
   590  	if r.resumeHeight > 0 {
   591  		return r.resumeHeight
   592  	}
   593  	return r.startHeight
   594  }
   595  
   596  func (r *fetchTestRun) StopHeight() uint64 {
   597  	if r.stopHeight > 0 {
   598  		return r.stopHeight
   599  	}
   600  	return r.endHeight
   601  }
   602  
   603  func (r *fetchTestRun) FetchedExecutionData() receivedExecutionData {
   604  	if r.fetchedExecutionData == nil {
   605  		return make(receivedExecutionData, r.sealedCount)
   606  	}
   607  	return r.fetchedExecutionData
   608  }
   609  
   610  // IsLastSeal returns true if the provided blockID is the last expected sealed block for the test
   611  func (r *fetchTestRun) IsLastSeal(blockID flow.Identifier) bool {
   612  	stopHeight := r.StopHeight()
   613  	lastSeal := r.blocksByHeight[stopHeight].Payload.Seals[0].BlockID
   614  	return lastSeal == r.blocksByID[blockID].ID()
   615  }
   616  
   617  func (suite *ExecutionDataRequesterSuite) generateTestData(blockCount int, specialHeightFuncs map[uint64]testExecutionDataCallback) *fetchTestRun {
   618  	edsEntries := map[flow.Identifier]*testExecutionDataServiceEntry{}
   619  	blocksByHeight := map[uint64]*flow.Block{}
   620  	blocksByID := map[flow.Identifier]*flow.Block{}
   621  	resultsByID := map[flow.Identifier]*flow.ExecutionResult{}
   622  	resultsByBlockID := map[flow.Identifier]*flow.ExecutionResult{}
   623  	sealsByBlockID := map[flow.Identifier]*flow.Seal{}
   624  	executionDataByID := map[flow.Identifier]*execution_data.BlockExecutionData{}
   625  	executionDataIDByBlockID := map[flow.Identifier]flow.Identifier{}
   626  
   627  	sealedCount := blockCount - 4 // seals for blocks 1-96
   628  	firstSeal := blockCount - sealedCount
   629  
   630  	// genesis is block 0, we start syncing from block 1
   631  	startHeight := uint64(1)
   632  	endHeight := uint64(blockCount) - 1
   633  
   634  	// instantiate ExecutionDataService to generate correct CIDs
   635  	eds := execution_data.NewExecutionDataStore(suite.blobstore, execution_data.DefaultSerializer)
   636  
   637  	var previousBlock *flow.Block
   638  	var previousResult *flow.ExecutionResult
   639  	for i := 0; i < blockCount; i++ {
   640  		var seals []*flow.Header
   641  
   642  		if i >= firstSeal {
   643  			sealedBlock := blocksByHeight[uint64(i-firstSeal+1)]
   644  			seals = []*flow.Header{
   645  				sealedBlock.Header, // block 0 doesn't get sealed (it's pre-sealed in the genesis state)
   646  			}
   647  
   648  			sealsByBlockID[sealedBlock.ID()] = unittest.Seal.Fixture(
   649  				unittest.Seal.WithBlockID(sealedBlock.ID()),
   650  				unittest.Seal.WithResult(resultsByBlockID[sealedBlock.ID()]),
   651  			)
   652  
   653  			suite.T().Logf("block %d has seals for %d", i, seals[0].Height)
   654  		}
   655  
   656  		height := uint64(i)
   657  		block := buildBlock(height, previousBlock, seals)
   658  
   659  		ed := synctest.ExecutionDataFixture(block.ID())
   660  
   661  		cid, err := eds.AddExecutionData(context.Background(), ed)
   662  		require.NoError(suite.T(), err)
   663  
   664  		result := buildResult(block, cid, previousResult)
   665  
   666  		blocksByHeight[height] = block
   667  		blocksByID[block.ID()] = block
   668  		resultsByBlockID[block.ID()] = result
   669  		resultsByID[result.ID()] = result
   670  
   671  		// ignore all the data we don't need to verify the test
   672  		if i > 0 && i <= sealedCount {
   673  			executionDataByID[block.ID()] = ed
   674  			edsEntries[cid] = &testExecutionDataServiceEntry{ExecutionData: ed}
   675  			if fn, has := specialHeightFuncs[height]; has {
   676  				edsEntries[cid].fn = fn
   677  			}
   678  
   679  			executionDataIDByBlockID[block.ID()] = cid
   680  		}
   681  
   682  		previousBlock = block
   683  		previousResult = result
   684  	}
   685  
   686  	return &fetchTestRun{
   687  		sealedCount:              sealedCount,
   688  		startHeight:              startHeight,
   689  		endHeight:                endHeight,
   690  		blocksByHeight:           blocksByHeight,
   691  		blocksByID:               blocksByID,
   692  		resultsByBlockID:         resultsByBlockID,
   693  		resultsByID:              resultsByID,
   694  		sealsByBlockID:           sealsByBlockID,
   695  		executionDataByID:        executionDataByID,
   696  		executionDataEntries:     edsEntries,
   697  		executionDataIDByBlockID: executionDataIDByBlockID,
   698  		waitTimeout:              time.Second * 5,
   699  
   700  		maxSearchAhead: requester.DefaultMaxSearchAhead,
   701  		fetchTimeout:   requester.DefaultFetchTimeout,
   702  		retryDelay:     1 * time.Millisecond,
   703  		maxRetryDelay:  15 * time.Millisecond,
   704  	}
   705  }
   706  
   707  func buildBlock(height uint64, parent *flow.Block, seals []*flow.Header) *flow.Block {
   708  	if parent == nil {
   709  		return unittest.GenesisFixture()
   710  	}
   711  
   712  	if len(seals) == 0 {
   713  		return unittest.BlockWithParentFixture(parent.Header)
   714  	}
   715  
   716  	return unittest.BlockWithParentAndSeals(parent.Header, seals)
   717  }
   718  
   719  func buildResult(block *flow.Block, cid flow.Identifier, previousResult *flow.ExecutionResult) *flow.ExecutionResult {
   720  	opts := []func(result *flow.ExecutionResult){
   721  		unittest.WithBlock(block),
   722  		unittest.WithExecutionDataID(cid),
   723  	}
   724  
   725  	if previousResult != nil {
   726  		opts = append(opts, unittest.WithPreviousResult(*previousResult))
   727  	}
   728  
   729  	return unittest.ExecutionResultFixture(opts...)
   730  }
   731  
   732  func verifyFetchedExecutionData(t *testing.T, actual receivedExecutionData, cfg *fetchTestRun) {
   733  	expected := cfg.executionDataByID
   734  	assert.Len(t, actual, len(expected))
   735  
   736  	for i := 0; i < cfg.sealedCount; i++ {
   737  		height := cfg.startHeight + uint64(i)
   738  		block := cfg.blocksByHeight[height]
   739  		blockID := block.ID()
   740  
   741  		expectedED := expected[blockID]
   742  		actualED, has := actual[blockID]
   743  		assert.True(t, has, "missing execution data for block %v height %d", blockID, height)
   744  		if has {
   745  			assert.Equal(t, expectedED, actualED, "execution data for block %v doesn't match", blockID)
   746  		}
   747  	}
   748  }
   749  
   750  type mockSnapshot struct {
   751  	header *flow.Header
   752  	err    error
   753  	mu     sync.Mutex
   754  }
   755  
   756  func (m *mockSnapshot) set(header *flow.Header, err error) {
   757  	m.mu.Lock()
   758  	defer m.mu.Unlock()
   759  
   760  	m.header = header
   761  	m.err = err
   762  }
   763  
   764  func (m *mockSnapshot) Head() (*flow.Header, error) {
   765  	m.mu.Lock()
   766  	defer m.mu.Unlock()
   767  
   768  	return m.header, m.err
   769  }
   770  
   771  // none of these are used in this test
   772  func (m *mockSnapshot) QuorumCertificate() (*flow.QuorumCertificate, error) { return nil, nil }
   773  func (m *mockSnapshot) Identities(selector flow.IdentityFilter) (flow.IdentityList, error) {
   774  	return nil, nil
   775  }
   776  func (m *mockSnapshot) Identity(nodeID flow.Identifier) (*flow.Identity, error) { return nil, nil }
   777  func (m *mockSnapshot) SealedResult() (*flow.ExecutionResult, *flow.Seal, error) {
   778  	return nil, nil, nil
   779  }
   780  func (m *mockSnapshot) Commit() (flow.StateCommitment, error)         { return flow.DummyStateCommitment, nil }
   781  func (m *mockSnapshot) SealingSegment() (*flow.SealingSegment, error) { return nil, nil }
   782  func (m *mockSnapshot) Descendants() ([]flow.Identifier, error)       { return nil, nil }
   783  func (m *mockSnapshot) ValidDescendants() ([]flow.Identifier, error)  { return nil, nil }
   784  func (m *mockSnapshot) RandomSource() ([]byte, error)                 { return nil, nil }
   785  func (m *mockSnapshot) Phase() (flow.EpochPhase, error)               { return flow.EpochPhaseUndefined, nil }
   786  func (m *mockSnapshot) Epochs() protocol.EpochQuery                   { return nil }
   787  func (m *mockSnapshot) Params() protocol.GlobalParams                 { return nil }