github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/state_stream/backend/backend_executiondata_test.go (about)

     1  package backend
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/ipfs/go-datastore"
    10  	dssync "github.com/ipfs/go-datastore/sync"
    11  	"github.com/rs/zerolog"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/mock"
    14  	"github.com/stretchr/testify/require"
    15  	"github.com/stretchr/testify/suite"
    16  	"google.golang.org/grpc/codes"
    17  	"google.golang.org/grpc/status"
    18  
    19  	"github.com/onflow/flow-go/engine"
    20  	"github.com/onflow/flow-go/engine/access/index"
    21  	"github.com/onflow/flow-go/engine/access/state_stream"
    22  	"github.com/onflow/flow-go/engine/access/subscription"
    23  	subscriptionmock "github.com/onflow/flow-go/engine/access/subscription/mock"
    24  	"github.com/onflow/flow-go/model/flow"
    25  	"github.com/onflow/flow-go/module/blobs"
    26  	"github.com/onflow/flow-go/module/execution"
    27  	"github.com/onflow/flow-go/module/executiondatasync/execution_data"
    28  	"github.com/onflow/flow-go/module/executiondatasync/execution_data/cache"
    29  	"github.com/onflow/flow-go/module/mempool/herocache"
    30  	"github.com/onflow/flow-go/module/metrics"
    31  	protocolmock "github.com/onflow/flow-go/state/protocol/mock"
    32  	"github.com/onflow/flow-go/storage"
    33  	storagemock "github.com/onflow/flow-go/storage/mock"
    34  	"github.com/onflow/flow-go/utils/unittest"
    35  	"github.com/onflow/flow-go/utils/unittest/mocks"
    36  )
    37  
    38  var (
    39  	chainID        = flow.MonotonicEmulator
    40  	testEventTypes = []flow.EventType{
    41  		unittest.EventTypeFixture(chainID),
    42  		unittest.EventTypeFixture(chainID),
    43  		unittest.EventTypeFixture(chainID),
    44  	}
    45  )
    46  
    47  type BackendExecutionDataSuite struct {
    48  	suite.Suite
    49  	logger         zerolog.Logger
    50  	state          *protocolmock.State
    51  	params         *protocolmock.Params
    52  	snapshot       *protocolmock.Snapshot
    53  	headers        *storagemock.Headers
    54  	events         *storagemock.Events
    55  	seals          *storagemock.Seals
    56  	results        *storagemock.ExecutionResults
    57  	registers      *storagemock.RegisterIndex
    58  	registersAsync *execution.RegistersAsyncStore
    59  	eventsIndex    *index.EventsIndex
    60  
    61  	bs                       blobs.Blobstore
    62  	eds                      execution_data.ExecutionDataStore
    63  	broadcaster              *engine.Broadcaster
    64  	execDataCache            *cache.ExecutionDataCache
    65  	execDataHeroCache        *herocache.BlockExecutionData
    66  	executionDataTracker     *subscriptionmock.ExecutionDataTracker
    67  	backend                  *StateStreamBackend
    68  	executionDataTrackerReal subscription.ExecutionDataTracker
    69  
    70  	blocks      []*flow.Block
    71  	blockEvents map[flow.Identifier][]flow.Event
    72  	execDataMap map[flow.Identifier]*execution_data.BlockExecutionDataEntity
    73  	blockMap    map[uint64]*flow.Block
    74  	sealMap     map[flow.Identifier]*flow.Seal
    75  	resultMap   map[flow.Identifier]*flow.ExecutionResult
    76  	registerID  flow.RegisterID
    77  
    78  	rootBlock          flow.Block
    79  	highestBlockHeader *flow.Header
    80  }
    81  
    82  type executionDataTestType struct {
    83  	name            string
    84  	highestBackfill int
    85  	startBlockID    flow.Identifier
    86  	startHeight     uint64
    87  }
    88  
    89  func TestBackendExecutionDataSuite(t *testing.T) {
    90  	suite.Run(t, new(BackendExecutionDataSuite))
    91  }
    92  
    93  func (s *BackendExecutionDataSuite) SetupTest() {
    94  	blockCount := 5
    95  	s.SetupTestSuite(blockCount)
    96  
    97  	var err error
    98  	parent := s.rootBlock.Header
    99  
   100  	for i := 0; i < blockCount; i++ {
   101  		block := unittest.BlockWithParentFixture(parent)
   102  		// update for next iteration
   103  		parent = block.Header
   104  
   105  		seal := unittest.BlockSealsFixture(1)[0]
   106  		result := unittest.ExecutionResultFixture()
   107  		blockEvents := generateMockEvents(block.Header, (i%len(testEventTypes))*3+1)
   108  
   109  		numChunks := 5
   110  		chunkDatas := make([]*execution_data.ChunkExecutionData, 0, numChunks)
   111  		for i := 0; i < numChunks; i++ {
   112  			var events flow.EventsList
   113  			switch {
   114  			case i >= len(blockEvents.Events):
   115  				events = flow.EventsList{}
   116  			case i == numChunks-1:
   117  				events = blockEvents.Events[i:]
   118  			default:
   119  				events = flow.EventsList{blockEvents.Events[i]}
   120  			}
   121  			chunkDatas = append(chunkDatas, unittest.ChunkExecutionDataFixture(s.T(), execution_data.DefaultMaxBlobSize/5, unittest.WithChunkEvents(events)))
   122  		}
   123  		execData := unittest.BlockExecutionDataFixture(
   124  			unittest.WithBlockExecutionDataBlockID(block.ID()),
   125  			unittest.WithChunkExecutionDatas(chunkDatas...),
   126  		)
   127  
   128  		result.ExecutionDataID, err = s.eds.Add(context.TODO(), execData)
   129  		assert.NoError(s.T(), err)
   130  
   131  		s.blocks = append(s.blocks, block)
   132  		s.execDataMap[block.ID()] = execution_data.NewBlockExecutionDataEntity(result.ExecutionDataID, execData)
   133  		s.blockEvents[block.ID()] = blockEvents.Events
   134  		s.blockMap[block.Header.Height] = block
   135  		s.sealMap[block.ID()] = seal
   136  		s.resultMap[seal.ResultID] = result
   137  
   138  		s.T().Logf("adding exec data for block %d %d %v => %v", i, block.Header.Height, block.ID(), result.ExecutionDataID)
   139  	}
   140  
   141  	s.SetupTestMocks()
   142  }
   143  
   144  func (s *BackendExecutionDataSuite) SetupTestSuite(blockCount int) {
   145  	s.logger = unittest.Logger()
   146  
   147  	s.state = protocolmock.NewState(s.T())
   148  	s.snapshot = protocolmock.NewSnapshot(s.T())
   149  	s.params = protocolmock.NewParams(s.T())
   150  	s.headers = storagemock.NewHeaders(s.T())
   151  	s.events = storagemock.NewEvents(s.T())
   152  	s.seals = storagemock.NewSeals(s.T())
   153  	s.results = storagemock.NewExecutionResults(s.T())
   154  
   155  	s.bs = blobs.NewBlobstore(dssync.MutexWrap(datastore.NewMapDatastore()))
   156  	s.eds = execution_data.NewExecutionDataStore(s.bs, execution_data.DefaultSerializer)
   157  
   158  	s.broadcaster = engine.NewBroadcaster()
   159  
   160  	s.execDataHeroCache = herocache.NewBlockExecutionData(subscription.DefaultCacheSize, s.logger, metrics.NewNoopCollector())
   161  	s.execDataCache = cache.NewExecutionDataCache(s.eds, s.headers, s.seals, s.results, s.execDataHeroCache)
   162  	s.executionDataTracker = subscriptionmock.NewExecutionDataTracker(s.T())
   163  
   164  	s.execDataMap = make(map[flow.Identifier]*execution_data.BlockExecutionDataEntity, blockCount)
   165  	s.blockEvents = make(map[flow.Identifier][]flow.Event, blockCount)
   166  	s.blockMap = make(map[uint64]*flow.Block, blockCount)
   167  	s.sealMap = make(map[flow.Identifier]*flow.Seal, blockCount)
   168  	s.resultMap = make(map[flow.Identifier]*flow.ExecutionResult, blockCount)
   169  	s.blocks = make([]*flow.Block, 0, blockCount)
   170  
   171  	// generate blockCount consecutive blocks with associated seal, result and execution data
   172  	s.rootBlock = unittest.BlockFixture()
   173  	s.blockMap[s.rootBlock.Header.Height] = &s.rootBlock
   174  	s.highestBlockHeader = s.rootBlock.Header
   175  
   176  	s.T().Logf("Generating %d blocks, root block: %d %s", blockCount, s.rootBlock.Header.Height, s.rootBlock.ID())
   177  }
   178  
   179  func (s *BackendExecutionDataSuite) SetupTestMocks() {
   180  	s.registerID = unittest.RegisterIDFixture()
   181  
   182  	s.eventsIndex = index.NewEventsIndex(s.events)
   183  	s.registersAsync = execution.NewRegistersAsyncStore()
   184  	s.registers = storagemock.NewRegisterIndex(s.T())
   185  	err := s.registersAsync.Initialize(s.registers)
   186  	require.NoError(s.T(), err)
   187  	s.registers.On("LatestHeight").Return(s.rootBlock.Header.Height).Maybe()
   188  	s.registers.On("FirstHeight").Return(s.rootBlock.Header.Height).Maybe()
   189  	s.registers.On("Get", mock.AnythingOfType("RegisterID"), mock.AnythingOfType("uint64")).Return(
   190  		func(id flow.RegisterID, height uint64) (flow.RegisterValue, error) {
   191  			if id == s.registerID {
   192  				return flow.RegisterValue{}, nil
   193  			}
   194  			return nil, storage.ErrNotFound
   195  		}).Maybe()
   196  
   197  	s.state.On("Sealed").Return(s.snapshot, nil).Maybe()
   198  	s.snapshot.On("Head").Return(s.blocks[0].Header, nil).Maybe()
   199  
   200  	s.seals.On("FinalizedSealForBlock", mock.AnythingOfType("flow.Identifier")).Return(
   201  		mocks.StorageMapGetter(s.sealMap),
   202  	).Maybe()
   203  
   204  	s.results.On("ByID", mock.AnythingOfType("flow.Identifier")).Return(
   205  		mocks.StorageMapGetter(s.resultMap),
   206  	).Maybe()
   207  
   208  	s.headers.On("ByBlockID", mock.AnythingOfType("flow.Identifier")).Return(
   209  		func(blockID flow.Identifier) (*flow.Header, error) {
   210  			for _, block := range s.blockMap {
   211  				if block.ID() == blockID {
   212  					return block.Header, nil
   213  				}
   214  			}
   215  			return nil, storage.ErrNotFound
   216  		},
   217  	).Maybe()
   218  
   219  	s.headers.On("ByHeight", mock.AnythingOfType("uint64")).Return(
   220  		mocks.ConvertStorageOutput(
   221  			mocks.StorageMapGetter(s.blockMap),
   222  			func(block *flow.Block) *flow.Header { return block.Header },
   223  		),
   224  	).Maybe()
   225  
   226  	s.headers.On("BlockIDByHeight", mock.AnythingOfType("uint64")).Return(
   227  		mocks.ConvertStorageOutput(
   228  			mocks.StorageMapGetter(s.blockMap),
   229  			func(block *flow.Block) flow.Identifier { return block.ID() },
   230  		),
   231  	).Maybe()
   232  
   233  	s.SetupBackend(false)
   234  }
   235  
   236  func (s *BackendExecutionDataSuite) SetupBackend(useEventsIndex bool) {
   237  	var err error
   238  	s.backend, err = New(
   239  		s.logger,
   240  		s.state,
   241  		s.headers,
   242  		s.seals,
   243  		s.results,
   244  		s.eds,
   245  		s.execDataCache,
   246  		s.registersAsync,
   247  		s.eventsIndex,
   248  		useEventsIndex,
   249  		state_stream.DefaultRegisterIDsRequestLimit,
   250  		subscription.NewSubscriptionHandler(
   251  			s.logger,
   252  			s.broadcaster,
   253  			subscription.DefaultSendTimeout,
   254  			subscription.DefaultResponseLimit,
   255  			subscription.DefaultSendBufferSize,
   256  		),
   257  		s.executionDataTracker,
   258  	)
   259  	require.NoError(s.T(), err)
   260  
   261  	// create real execution data tracker to use GetStartHeight from it, instead of mocking
   262  	s.executionDataTrackerReal = subscription.NewExecutionDataTracker(
   263  		s.logger,
   264  		s.state,
   265  		s.rootBlock.Header.Height,
   266  		s.headers,
   267  		s.broadcaster,
   268  		s.rootBlock.Header.Height,
   269  		s.eventsIndex,
   270  		useEventsIndex,
   271  	)
   272  
   273  	s.executionDataTracker.On(
   274  		"GetStartHeight",
   275  		mock.Anything,
   276  		mock.Anything,
   277  		mock.Anything,
   278  	).Return(func(ctx context.Context, startBlockID flow.Identifier, startHeight uint64) (uint64, error) {
   279  		return s.executionDataTrackerReal.GetStartHeight(ctx, startBlockID, startHeight)
   280  	}, nil).Maybe()
   281  
   282  	s.executionDataTracker.On("GetHighestHeight").Return(func() uint64 {
   283  		return s.highestBlockHeader.Height
   284  	}).Maybe()
   285  }
   286  
   287  // generateMockEvents generates a set of mock events for a block split into multiple tx with
   288  // appropriate indexes set
   289  func generateMockEvents(header *flow.Header, eventCount int) flow.BlockEvents {
   290  	txCount := eventCount / 3
   291  
   292  	txID := unittest.IdentifierFixture()
   293  	txIndex := uint32(0)
   294  	eventIndex := uint32(0)
   295  
   296  	events := make([]flow.Event, eventCount)
   297  	for i := 0; i < eventCount; i++ {
   298  		if i > 0 && i%txCount == 0 {
   299  			txIndex++
   300  			txID = unittest.IdentifierFixture()
   301  			eventIndex = 0
   302  		}
   303  
   304  		events[i] = unittest.EventFixture(testEventTypes[i%len(testEventTypes)], txIndex, eventIndex, txID, 0)
   305  	}
   306  
   307  	return flow.BlockEvents{
   308  		BlockID:        header.ID(),
   309  		BlockHeight:    header.Height,
   310  		BlockTimestamp: header.Timestamp,
   311  		Events:         events,
   312  	}
   313  }
   314  
   315  func (s *BackendExecutionDataSuite) TestGetExecutionDataByBlockID() {
   316  	ctx, cancel := context.WithCancel(context.Background())
   317  	defer cancel()
   318  
   319  	block := s.blocks[0]
   320  	seal := s.sealMap[block.ID()]
   321  	result := s.resultMap[seal.ResultID]
   322  	execData := s.execDataMap[block.ID()]
   323  
   324  	// notify backend block is available
   325  	s.highestBlockHeader = block.Header
   326  
   327  	var err error
   328  	s.Run("happy path TestGetExecutionDataByBlockID success", func() {
   329  		result.ExecutionDataID, err = s.eds.Add(ctx, execData.BlockExecutionData)
   330  		require.NoError(s.T(), err)
   331  
   332  		res, err := s.backend.GetExecutionDataByBlockID(ctx, block.ID())
   333  		assert.Equal(s.T(), execData.BlockExecutionData, res)
   334  		assert.NoError(s.T(), err)
   335  	})
   336  
   337  	s.execDataHeroCache.Clear()
   338  
   339  	s.Run("missing exec data for TestGetExecutionDataByBlockID failure", func() {
   340  		result.ExecutionDataID = unittest.IdentifierFixture()
   341  
   342  		execDataRes, err := s.backend.GetExecutionDataByBlockID(ctx, block.ID())
   343  		assert.Nil(s.T(), execDataRes)
   344  		assert.Equal(s.T(), codes.NotFound, status.Code(err))
   345  	})
   346  }
   347  
   348  func (s *BackendExecutionDataSuite) TestSubscribeExecutionData() {
   349  	tests := []executionDataTestType{
   350  		{
   351  			name:            "happy path - all new blocks",
   352  			highestBackfill: -1, // no backfill
   353  			startBlockID:    flow.ZeroID,
   354  			startHeight:     0,
   355  		},
   356  		{
   357  			name:            "happy path - partial backfill",
   358  			highestBackfill: 2, // backfill the first 3 blocks
   359  			startBlockID:    flow.ZeroID,
   360  			startHeight:     s.blocks[0].Header.Height,
   361  		},
   362  		{
   363  			name:            "happy path - complete backfill",
   364  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   365  			startBlockID:    s.blocks[0].ID(),
   366  			startHeight:     0,
   367  		},
   368  		{
   369  			name:            "happy path - start from root block by height",
   370  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   371  			startBlockID:    flow.ZeroID,
   372  			startHeight:     s.rootBlock.Header.Height, // start from root block
   373  		},
   374  		{
   375  			name:            "happy path - start from root block by id",
   376  			highestBackfill: len(s.blocks) - 1,       // backfill all blocks
   377  			startBlockID:    s.rootBlock.Header.ID(), // start from root block
   378  			startHeight:     0,
   379  		},
   380  	}
   381  
   382  	subFunc := func(ctx context.Context, blockID flow.Identifier, startHeight uint64) subscription.Subscription {
   383  		return s.backend.SubscribeExecutionData(ctx, blockID, startHeight)
   384  	}
   385  
   386  	s.subscribe(subFunc, tests)
   387  }
   388  
   389  func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataFromStartBlockID() {
   390  	tests := []executionDataTestType{
   391  		{
   392  			name:            "happy path - all new blocks",
   393  			highestBackfill: -1, // no backfill
   394  			startBlockID:    s.rootBlock.ID(),
   395  		},
   396  		{
   397  			name:            "happy path - partial backfill",
   398  			highestBackfill: 2, // backfill the first 3 blocks
   399  			startBlockID:    s.blocks[0].ID(),
   400  		},
   401  		{
   402  			name:            "happy path - complete backfill",
   403  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   404  			startBlockID:    s.blocks[0].ID(),
   405  		},
   406  		{
   407  			name:            "happy path - start from root block by id",
   408  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   409  			startBlockID:    s.rootBlock.ID(),  // start from root block
   410  		},
   411  	}
   412  
   413  	s.executionDataTracker.On(
   414  		"GetStartHeightFromBlockID",
   415  		mock.AnythingOfType("flow.Identifier"),
   416  	).Return(func(startBlockID flow.Identifier) (uint64, error) {
   417  		return s.executionDataTrackerReal.GetStartHeightFromBlockID(startBlockID)
   418  	}, nil)
   419  
   420  	subFunc := func(ctx context.Context, blockID flow.Identifier, startHeight uint64) subscription.Subscription {
   421  		return s.backend.SubscribeExecutionDataFromStartBlockID(ctx, blockID)
   422  	}
   423  
   424  	s.subscribe(subFunc, tests)
   425  }
   426  
   427  func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataFromStartBlockHeight() {
   428  	tests := []executionDataTestType{
   429  		{
   430  			name:            "happy path - all new blocks",
   431  			highestBackfill: -1, // no backfill
   432  			startHeight:     s.rootBlock.Header.Height,
   433  		},
   434  		{
   435  			name:            "happy path - partial backfill",
   436  			highestBackfill: 2, // backfill the first 3 blocks
   437  			startHeight:     s.blocks[0].Header.Height,
   438  		},
   439  		{
   440  			name:            "happy path - complete backfill",
   441  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   442  			startHeight:     s.blocks[0].Header.Height,
   443  		},
   444  		{
   445  			name:            "happy path - start from root block by id",
   446  			highestBackfill: len(s.blocks) - 1,         // backfill all blocks
   447  			startHeight:     s.rootBlock.Header.Height, // start from root block
   448  		},
   449  	}
   450  
   451  	s.executionDataTracker.On(
   452  		"GetStartHeightFromHeight",
   453  		mock.AnythingOfType("uint64"),
   454  	).Return(func(startHeight uint64) (uint64, error) {
   455  		return s.executionDataTrackerReal.GetStartHeightFromHeight(startHeight)
   456  	}, nil)
   457  
   458  	subFunc := func(ctx context.Context, blockID flow.Identifier, startHeight uint64) subscription.Subscription {
   459  		return s.backend.SubscribeExecutionDataFromStartBlockHeight(ctx, startHeight)
   460  	}
   461  
   462  	s.subscribe(subFunc, tests)
   463  }
   464  
   465  func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataFromLatest() {
   466  	tests := []executionDataTestType{
   467  		{
   468  			name:            "happy path - all new blocks",
   469  			highestBackfill: -1, // no backfill
   470  		},
   471  		{
   472  			name:            "happy path - partial backfill",
   473  			highestBackfill: 2, // backfill the first 3 blocks
   474  		},
   475  		{
   476  			name:            "happy path - complete backfill",
   477  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   478  		},
   479  	}
   480  
   481  	s.executionDataTracker.On(
   482  		"GetStartHeightFromLatest",
   483  		mock.Anything,
   484  	).Return(func(ctx context.Context) (uint64, error) {
   485  		return s.executionDataTrackerReal.GetStartHeightFromLatest(ctx)
   486  	}, nil)
   487  
   488  	subFunc := func(ctx context.Context, blockID flow.Identifier, startHeight uint64) subscription.Subscription {
   489  		return s.backend.SubscribeExecutionDataFromLatest(ctx)
   490  	}
   491  
   492  	s.subscribe(subFunc, tests)
   493  }
   494  
   495  func (s *BackendExecutionDataSuite) subscribe(subscribeFunc func(ctx context.Context, startBlockID flow.Identifier, startHeight uint64) subscription.Subscription, tests []executionDataTestType) {
   496  	ctx, cancel := context.WithCancel(context.Background())
   497  	defer cancel()
   498  
   499  	for _, test := range tests {
   500  		s.Run(test.name, func() {
   501  			// make sure we're starting with a fresh cache
   502  			s.execDataHeroCache.Clear()
   503  
   504  			s.T().Logf("len(s.execDataMap) %d", len(s.execDataMap))
   505  
   506  			// add "backfill" block - blocks that are already in the database before the test starts
   507  			// this simulates a subscription on a past block
   508  			for i := 0; i <= test.highestBackfill; i++ {
   509  				s.T().Logf("backfilling block %d", i)
   510  				s.highestBlockHeader = s.blocks[i].Header
   511  			}
   512  
   513  			subCtx, subCancel := context.WithCancel(ctx)
   514  			sub := subscribeFunc(subCtx, test.startBlockID, test.startHeight)
   515  
   516  			// loop over of the all blocks
   517  			for i, b := range s.blocks {
   518  				execData := s.execDataMap[b.ID()]
   519  				s.T().Logf("checking block %d %v %v", i, b.Header.Height, b.ID())
   520  
   521  				// simulate new exec data received.
   522  				// exec data for all blocks with index <= highestBackfill were already received
   523  				if i > test.highestBackfill {
   524  					s.highestBlockHeader = b.Header
   525  					s.broadcaster.Publish()
   526  				}
   527  
   528  				// consume execution data from subscription
   529  				unittest.RequireReturnsBefore(s.T(), func() {
   530  					v, ok := <-sub.Channel()
   531  					require.True(s.T(), ok, "channel closed while waiting for exec data for block %d %v: err: %v", b.Header.Height, b.ID(), sub.Err())
   532  
   533  					resp, ok := v.(*ExecutionDataResponse)
   534  					require.True(s.T(), ok, "unexpected response type: %T", v)
   535  
   536  					assert.Equal(s.T(), b.Header.Height, resp.Height)
   537  					assert.Equal(s.T(), execData.BlockExecutionData, resp.ExecutionData)
   538  				}, time.Second, fmt.Sprintf("timed out waiting for exec data for block %d %v", b.Header.Height, b.ID()))
   539  			}
   540  
   541  			// make sure there are no new messages waiting. the channel should be opened with nothing waiting
   542  			unittest.RequireNeverReturnBefore(s.T(), func() {
   543  				<-sub.Channel()
   544  			}, 100*time.Millisecond, "timed out waiting for subscription to shutdown")
   545  
   546  			// stop the subscription
   547  			subCancel()
   548  
   549  			// ensure subscription shuts down gracefully
   550  			unittest.RequireReturnsBefore(s.T(), func() {
   551  				v, ok := <-sub.Channel()
   552  				assert.Nil(s.T(), v)
   553  				assert.False(s.T(), ok)
   554  				assert.ErrorIs(s.T(), sub.Err(), context.Canceled)
   555  			}, 100*time.Millisecond, "timed out waiting for subscription to shutdown")
   556  		})
   557  	}
   558  }
   559  
   560  func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataHandlesErrors() {
   561  	ctx, cancel := context.WithCancel(context.Background())
   562  	defer cancel()
   563  
   564  	s.Run("returns error if both start blockID and start height are provided", func() {
   565  		subCtx, subCancel := context.WithCancel(ctx)
   566  		defer subCancel()
   567  
   568  		sub := s.backend.SubscribeExecutionData(subCtx, unittest.IdentifierFixture(), 1)
   569  		assert.Equal(s.T(), codes.InvalidArgument, status.Code(sub.Err()))
   570  	})
   571  
   572  	s.Run("returns error for start height before root height", func() {
   573  		subCtx, subCancel := context.WithCancel(ctx)
   574  		defer subCancel()
   575  
   576  		sub := s.backend.SubscribeExecutionData(subCtx, flow.ZeroID, s.rootBlock.Header.Height-1)
   577  		assert.Equal(s.T(), codes.InvalidArgument, status.Code(sub.Err()))
   578  	})
   579  
   580  	s.Run("returns error for unindexed start blockID", func() {
   581  		subCtx, subCancel := context.WithCancel(ctx)
   582  		defer subCancel()
   583  
   584  		sub := s.backend.SubscribeExecutionData(subCtx, unittest.IdentifierFixture(), 0)
   585  		assert.Equal(s.T(), codes.NotFound, status.Code(sub.Err()))
   586  	})
   587  
   588  	// make sure we're starting with a fresh cache
   589  	s.execDataHeroCache.Clear()
   590  
   591  	s.Run("returns error for unindexed start height", func() {
   592  		subCtx, subCancel := context.WithCancel(ctx)
   593  		defer subCancel()
   594  
   595  		sub := s.backend.SubscribeExecutionData(subCtx, flow.ZeroID, s.blocks[len(s.blocks)-1].Header.Height+10)
   596  		assert.Equal(s.T(), codes.NotFound, status.Code(sub.Err()))
   597  	})
   598  }
   599  
   600  func (s *BackendExecutionDataSuite) TestGetRegisterValues() {
   601  	s.Run("normal case", func() {
   602  		res, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, s.rootBlock.Header.Height)
   603  		require.NoError(s.T(), err)
   604  		require.NotEmpty(s.T(), res)
   605  	})
   606  
   607  	s.Run("returns error if block height is out of range", func() {
   608  		res, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, s.rootBlock.Header.Height+1)
   609  		require.Nil(s.T(), res)
   610  		require.Equal(s.T(), codes.OutOfRange, status.Code(err))
   611  	})
   612  
   613  	s.Run("returns error if register path is not indexed", func() {
   614  		falseID := flow.RegisterIDs{flow.RegisterID{Owner: "ha", Key: "ha"}}
   615  		res, err := s.backend.GetRegisterValues(falseID, s.rootBlock.Header.Height)
   616  		require.Nil(s.T(), res)
   617  		require.Equal(s.T(), codes.NotFound, status.Code(err))
   618  	})
   619  
   620  	s.Run("returns error if too many registers are requested", func() {
   621  		res, err := s.backend.GetRegisterValues(make(flow.RegisterIDs, s.backend.registerRequestLimit+1), s.rootBlock.Header.Height)
   622  		require.Nil(s.T(), res)
   623  		require.Equal(s.T(), codes.InvalidArgument, status.Code(err))
   624  	})
   625  }