github.com/onflow/flow-go@v0.33.17/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/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/mock"
    13  	"github.com/stretchr/testify/require"
    14  	"github.com/stretchr/testify/suite"
    15  	"google.golang.org/grpc/codes"
    16  	"google.golang.org/grpc/status"
    17  
    18  	"github.com/onflow/flow-go/engine"
    19  	"github.com/onflow/flow-go/engine/access/rpc/backend"
    20  	"github.com/onflow/flow-go/engine/access/state_stream"
    21  	"github.com/onflow/flow-go/model/flow"
    22  	"github.com/onflow/flow-go/module/blobs"
    23  	"github.com/onflow/flow-go/module/execution"
    24  	"github.com/onflow/flow-go/module/executiondatasync/execution_data"
    25  	"github.com/onflow/flow-go/module/executiondatasync/execution_data/cache"
    26  	"github.com/onflow/flow-go/module/mempool/herocache"
    27  	"github.com/onflow/flow-go/module/metrics"
    28  	protocolmock "github.com/onflow/flow-go/state/protocol/mock"
    29  	"github.com/onflow/flow-go/storage"
    30  	storagemock "github.com/onflow/flow-go/storage/mock"
    31  	"github.com/onflow/flow-go/utils/unittest"
    32  	"github.com/onflow/flow-go/utils/unittest/mocks"
    33  )
    34  
    35  var chainID = flow.MonotonicEmulator
    36  var testEventTypes = []flow.EventType{
    37  	unittest.EventTypeFixture(chainID),
    38  	unittest.EventTypeFixture(chainID),
    39  	unittest.EventTypeFixture(chainID),
    40  }
    41  
    42  type BackendExecutionDataSuite struct {
    43  	suite.Suite
    44  
    45  	state          *protocolmock.State
    46  	params         *protocolmock.Params
    47  	snapshot       *protocolmock.Snapshot
    48  	headers        *storagemock.Headers
    49  	events         *storagemock.Events
    50  	seals          *storagemock.Seals
    51  	results        *storagemock.ExecutionResults
    52  	registers      *storagemock.RegisterIndex
    53  	registersAsync *execution.RegistersAsyncStore
    54  	eventsIndex    *backend.EventsIndex
    55  
    56  	bs                blobs.Blobstore
    57  	eds               execution_data.ExecutionDataStore
    58  	broadcaster       *engine.Broadcaster
    59  	execDataCache     *cache.ExecutionDataCache
    60  	execDataHeroCache *herocache.BlockExecutionData
    61  	backend           *StateStreamBackend
    62  
    63  	blocks      []*flow.Block
    64  	blockEvents map[flow.Identifier][]flow.Event
    65  	execDataMap map[flow.Identifier]*execution_data.BlockExecutionDataEntity
    66  	blockMap    map[uint64]*flow.Block
    67  	sealMap     map[flow.Identifier]*flow.Seal
    68  	resultMap   map[flow.Identifier]*flow.ExecutionResult
    69  	registerID  flow.RegisterID
    70  }
    71  
    72  func TestBackendExecutionDataSuite(t *testing.T) {
    73  	suite.Run(t, new(BackendExecutionDataSuite))
    74  }
    75  
    76  func (s *BackendExecutionDataSuite) SetupTest() {
    77  	logger := unittest.Logger()
    78  
    79  	s.state = protocolmock.NewState(s.T())
    80  	s.snapshot = protocolmock.NewSnapshot(s.T())
    81  	s.params = protocolmock.NewParams(s.T())
    82  	s.headers = storagemock.NewHeaders(s.T())
    83  	s.events = storagemock.NewEvents(s.T())
    84  	s.seals = storagemock.NewSeals(s.T())
    85  	s.results = storagemock.NewExecutionResults(s.T())
    86  
    87  	s.bs = blobs.NewBlobstore(dssync.MutexWrap(datastore.NewMapDatastore()))
    88  	s.eds = execution_data.NewExecutionDataStore(s.bs, execution_data.DefaultSerializer)
    89  
    90  	s.broadcaster = engine.NewBroadcaster()
    91  
    92  	s.execDataHeroCache = herocache.NewBlockExecutionData(state_stream.DefaultCacheSize, logger, metrics.NewNoopCollector())
    93  	s.execDataCache = cache.NewExecutionDataCache(s.eds, s.headers, s.seals, s.results, s.execDataHeroCache)
    94  
    95  	conf := Config{
    96  		ClientSendTimeout:       state_stream.DefaultSendTimeout,
    97  		ClientSendBufferSize:    state_stream.DefaultSendBufferSize,
    98  		RegisterIDsRequestLimit: state_stream.DefaultRegisterIDsRequestLimit,
    99  	}
   100  
   101  	var err error
   102  
   103  	blockCount := 5
   104  	s.execDataMap = make(map[flow.Identifier]*execution_data.BlockExecutionDataEntity, blockCount)
   105  	s.blockEvents = make(map[flow.Identifier][]flow.Event, blockCount)
   106  	s.blockMap = make(map[uint64]*flow.Block, blockCount)
   107  	s.sealMap = make(map[flow.Identifier]*flow.Seal, blockCount)
   108  	s.resultMap = make(map[flow.Identifier]*flow.ExecutionResult, blockCount)
   109  	s.blocks = make([]*flow.Block, 0, blockCount)
   110  
   111  	// generate blockCount consecutive blocks with associated seal, result and execution data
   112  	rootBlock := unittest.BlockFixture()
   113  	parent := rootBlock.Header
   114  	s.blockMap[rootBlock.Header.Height] = &rootBlock
   115  
   116  	s.T().Logf("Generating %d blocks, root block: %d %s", blockCount, rootBlock.Header.Height, rootBlock.ID())
   117  
   118  	for i := 0; i < blockCount; i++ {
   119  		block := unittest.BlockWithParentFixture(parent)
   120  		// update for next iteration
   121  		parent = block.Header
   122  
   123  		seal := unittest.BlockSealsFixture(1)[0]
   124  		result := unittest.ExecutionResultFixture()
   125  		blockEvents := generateMockEvents(block.Header, (i%len(testEventTypes))*3+1)
   126  
   127  		numChunks := 5
   128  		chunkDatas := make([]*execution_data.ChunkExecutionData, 0, numChunks)
   129  		for i := 0; i < numChunks; i++ {
   130  			var events flow.EventsList
   131  			switch {
   132  			case i >= len(blockEvents.Events):
   133  				events = flow.EventsList{}
   134  			case i == numChunks-1:
   135  				events = blockEvents.Events[i:]
   136  			default:
   137  				events = flow.EventsList{blockEvents.Events[i]}
   138  			}
   139  			chunkDatas = append(chunkDatas, unittest.ChunkExecutionDataFixture(s.T(), execution_data.DefaultMaxBlobSize/5, unittest.WithChunkEvents(events)))
   140  		}
   141  		execData := unittest.BlockExecutionDataFixture(
   142  			unittest.WithBlockExecutionDataBlockID(block.ID()),
   143  			unittest.WithChunkExecutionDatas(chunkDatas...),
   144  		)
   145  
   146  		result.ExecutionDataID, err = s.eds.Add(context.TODO(), execData)
   147  		assert.NoError(s.T(), err)
   148  
   149  		s.blocks = append(s.blocks, block)
   150  		s.execDataMap[block.ID()] = execution_data.NewBlockExecutionDataEntity(result.ExecutionDataID, execData)
   151  		s.blockEvents[block.ID()] = blockEvents.Events
   152  		s.blockMap[block.Header.Height] = block
   153  		s.sealMap[block.ID()] = seal
   154  		s.resultMap[seal.ResultID] = result
   155  
   156  		s.T().Logf("adding exec data for block %d %d %v => %v", i, block.Header.Height, block.ID(), result.ExecutionDataID)
   157  	}
   158  
   159  	s.registerID = unittest.RegisterIDFixture()
   160  
   161  	s.eventsIndex = backend.NewEventsIndex(s.events)
   162  	s.registersAsync = execution.NewRegistersAsyncStore()
   163  	s.registers = storagemock.NewRegisterIndex(s.T())
   164  	err = s.registersAsync.Initialize(s.registers)
   165  	require.NoError(s.T(), err)
   166  	s.registers.On("LatestHeight").Return(rootBlock.Header.Height).Maybe()
   167  	s.registers.On("FirstHeight").Return(rootBlock.Header.Height).Maybe()
   168  	s.registers.On("Get", mock.AnythingOfType("RegisterID"), mock.AnythingOfType("uint64")).Return(
   169  		func(id flow.RegisterID, height uint64) (flow.RegisterValue, error) {
   170  			if id == s.registerID {
   171  				return flow.RegisterValue{}, nil
   172  			}
   173  			return nil, storage.ErrNotFound
   174  		}).Maybe()
   175  
   176  	s.state.On("Sealed").Return(s.snapshot, nil).Maybe()
   177  	s.snapshot.On("Head").Return(s.blocks[0].Header, nil).Maybe()
   178  
   179  	s.seals.On("FinalizedSealForBlock", mock.AnythingOfType("flow.Identifier")).Return(
   180  		mocks.StorageMapGetter(s.sealMap),
   181  	).Maybe()
   182  
   183  	s.results.On("ByID", mock.AnythingOfType("flow.Identifier")).Return(
   184  		mocks.StorageMapGetter(s.resultMap),
   185  	).Maybe()
   186  
   187  	s.headers.On("ByBlockID", mock.AnythingOfType("flow.Identifier")).Return(
   188  		func(blockID flow.Identifier) (*flow.Header, error) {
   189  			for _, block := range s.blockMap {
   190  				if block.ID() == blockID {
   191  					return block.Header, nil
   192  				}
   193  			}
   194  			return nil, storage.ErrNotFound
   195  		},
   196  	).Maybe()
   197  
   198  	s.headers.On("ByHeight", mock.AnythingOfType("uint64")).Return(
   199  		mocks.ConvertStorageOutput(
   200  			mocks.StorageMapGetter(s.blockMap),
   201  			func(block *flow.Block) *flow.Header { return block.Header },
   202  		),
   203  	).Maybe()
   204  
   205  	s.headers.On("BlockIDByHeight", mock.AnythingOfType("uint64")).Return(
   206  		mocks.ConvertStorageOutput(
   207  			mocks.StorageMapGetter(s.blockMap),
   208  			func(block *flow.Block) flow.Identifier { return block.ID() },
   209  		),
   210  	).Maybe()
   211  
   212  	s.backend, err = New(
   213  		logger,
   214  		conf,
   215  		s.state,
   216  		s.headers,
   217  		s.seals,
   218  		s.results,
   219  		s.eds,
   220  		s.execDataCache,
   221  		s.broadcaster,
   222  		rootBlock.Header.Height,
   223  		rootBlock.Header.Height, // initialize with no downloaded data
   224  		s.registersAsync,
   225  		s.eventsIndex,
   226  		false,
   227  	)
   228  	require.NoError(s.T(), err)
   229  }
   230  
   231  // generateMockEvents generates a set of mock events for a block split into multiple tx with
   232  // appropriate indexes set
   233  func generateMockEvents(header *flow.Header, eventCount int) flow.BlockEvents {
   234  	txCount := eventCount / 3
   235  
   236  	txID := unittest.IdentifierFixture()
   237  	txIndex := uint32(0)
   238  	eventIndex := uint32(0)
   239  
   240  	events := make([]flow.Event, eventCount)
   241  	for i := 0; i < eventCount; i++ {
   242  		if i > 0 && i%txCount == 0 {
   243  			txIndex++
   244  			txID = unittest.IdentifierFixture()
   245  			eventIndex = 0
   246  		}
   247  
   248  		events[i] = unittest.EventFixture(testEventTypes[i%len(testEventTypes)], txIndex, eventIndex, txID, 0)
   249  	}
   250  
   251  	return flow.BlockEvents{
   252  		BlockID:        header.ID(),
   253  		BlockHeight:    header.Height,
   254  		BlockTimestamp: header.Timestamp,
   255  		Events:         events,
   256  	}
   257  }
   258  
   259  func (s *BackendExecutionDataSuite) TestGetExecutionDataByBlockID() {
   260  	ctx, cancel := context.WithCancel(context.Background())
   261  	defer cancel()
   262  
   263  	block := s.blocks[0]
   264  	seal := s.sealMap[block.ID()]
   265  	result := s.resultMap[seal.ResultID]
   266  	execData := s.execDataMap[block.ID()]
   267  
   268  	// notify backend block is available
   269  	s.backend.setHighestHeight(block.Header.Height)
   270  
   271  	var err error
   272  	s.Run("happy path TestGetExecutionDataByBlockID success", func() {
   273  		result.ExecutionDataID, err = s.eds.Add(ctx, execData.BlockExecutionData)
   274  		require.NoError(s.T(), err)
   275  
   276  		res, err := s.backend.GetExecutionDataByBlockID(ctx, block.ID())
   277  		assert.Equal(s.T(), execData.BlockExecutionData, res)
   278  		assert.NoError(s.T(), err)
   279  	})
   280  
   281  	s.execDataHeroCache.Clear()
   282  
   283  	s.Run("missing exec data for TestGetExecutionDataByBlockID failure", func() {
   284  		result.ExecutionDataID = unittest.IdentifierFixture()
   285  
   286  		execDataRes, err := s.backend.GetExecutionDataByBlockID(ctx, block.ID())
   287  		assert.Nil(s.T(), execDataRes)
   288  		assert.Equal(s.T(), codes.NotFound, status.Code(err))
   289  	})
   290  }
   291  
   292  func (s *BackendExecutionDataSuite) TestSubscribeExecutionData() {
   293  	ctx, cancel := context.WithCancel(context.Background())
   294  	defer cancel()
   295  
   296  	tests := []struct {
   297  		name            string
   298  		highestBackfill int
   299  		startBlockID    flow.Identifier
   300  		startHeight     uint64
   301  	}{
   302  		{
   303  			name:            "happy path - all new blocks",
   304  			highestBackfill: -1, // no backfill
   305  			startBlockID:    flow.ZeroID,
   306  			startHeight:     0,
   307  		},
   308  		{
   309  			name:            "happy path - partial backfill",
   310  			highestBackfill: 2, // backfill the first 3 blocks
   311  			startBlockID:    flow.ZeroID,
   312  			startHeight:     s.blocks[0].Header.Height,
   313  		},
   314  		{
   315  			name:            "happy path - complete backfill",
   316  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   317  			startBlockID:    s.blocks[0].ID(),
   318  			startHeight:     0,
   319  		},
   320  		{
   321  			name:            "happy path - start from root block by height",
   322  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   323  			startBlockID:    flow.ZeroID,
   324  			startHeight:     s.backend.rootBlockHeight, // start from root block
   325  		},
   326  		{
   327  			name:            "happy path - start from root block by id",
   328  			highestBackfill: len(s.blocks) - 1,     // backfill all blocks
   329  			startBlockID:    s.backend.rootBlockID, // start from root block
   330  			startHeight:     0,
   331  		},
   332  	}
   333  
   334  	for _, test := range tests {
   335  		s.Run(test.name, func() {
   336  			// make sure we're starting with a fresh cache
   337  			s.execDataHeroCache.Clear()
   338  
   339  			s.T().Logf("len(s.execDataMap) %d", len(s.execDataMap))
   340  
   341  			// add "backfill" block - blocks that are already in the database before the test starts
   342  			// this simulates a subscription on a past block
   343  			for i := 0; i <= test.highestBackfill; i++ {
   344  				s.T().Logf("backfilling block %d", i)
   345  				s.backend.setHighestHeight(s.blocks[i].Header.Height)
   346  			}
   347  
   348  			subCtx, subCancel := context.WithCancel(ctx)
   349  			sub := s.backend.SubscribeExecutionData(subCtx, test.startBlockID, test.startHeight)
   350  
   351  			// loop over all of the blocks
   352  			for i, b := range s.blocks {
   353  				execData := s.execDataMap[b.ID()]
   354  				s.T().Logf("checking block %d %v", i, b.ID())
   355  
   356  				// simulate new exec data received.
   357  				// exec data for all blocks with index <= highestBackfill were already received
   358  				if i > test.highestBackfill {
   359  					s.backend.setHighestHeight(b.Header.Height)
   360  					s.broadcaster.Publish()
   361  				}
   362  
   363  				// consume execution data from subscription
   364  				unittest.RequireReturnsBefore(s.T(), func() {
   365  					v, ok := <-sub.Channel()
   366  					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())
   367  
   368  					resp, ok := v.(*ExecutionDataResponse)
   369  					require.True(s.T(), ok, "unexpected response type: %T", v)
   370  
   371  					assert.Equal(s.T(), b.Header.Height, resp.Height)
   372  					assert.Equal(s.T(), execData.BlockExecutionData, resp.ExecutionData)
   373  				}, time.Second, fmt.Sprintf("timed out waiting for exec data for block %d %v", b.Header.Height, b.ID()))
   374  			}
   375  
   376  			// make sure there are no new messages waiting. the channel should be opened with nothing waiting
   377  			unittest.RequireNeverReturnBefore(s.T(), func() {
   378  				<-sub.Channel()
   379  			}, 100*time.Millisecond, "timed out waiting for subscription to shutdown")
   380  
   381  			// stop the subscription
   382  			subCancel()
   383  
   384  			// ensure subscription shuts down gracefully
   385  			unittest.RequireReturnsBefore(s.T(), func() {
   386  				v, ok := <-sub.Channel()
   387  				assert.Nil(s.T(), v)
   388  				assert.False(s.T(), ok)
   389  				assert.ErrorIs(s.T(), sub.Err(), context.Canceled)
   390  			}, 100*time.Millisecond, "timed out waiting for subscription to shutdown")
   391  		})
   392  	}
   393  }
   394  
   395  func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataHandlesErrors() {
   396  	ctx, cancel := context.WithCancel(context.Background())
   397  	defer cancel()
   398  
   399  	s.Run("returns error if both start blockID and start height are provided", func() {
   400  		subCtx, subCancel := context.WithCancel(ctx)
   401  		defer subCancel()
   402  
   403  		sub := s.backend.SubscribeExecutionData(subCtx, unittest.IdentifierFixture(), 1)
   404  		assert.Equal(s.T(), codes.InvalidArgument, status.Code(sub.Err()))
   405  	})
   406  
   407  	s.Run("returns error for start height before root height", func() {
   408  		subCtx, subCancel := context.WithCancel(ctx)
   409  		defer subCancel()
   410  
   411  		sub := s.backend.SubscribeExecutionData(subCtx, flow.ZeroID, s.backend.rootBlockHeight-1)
   412  		assert.Equal(s.T(), codes.InvalidArgument, status.Code(sub.Err()))
   413  	})
   414  
   415  	s.Run("returns error for unindexed start blockID", func() {
   416  		subCtx, subCancel := context.WithCancel(ctx)
   417  		defer subCancel()
   418  
   419  		sub := s.backend.SubscribeExecutionData(subCtx, unittest.IdentifierFixture(), 0)
   420  		assert.Equal(s.T(), codes.NotFound, status.Code(sub.Err()))
   421  	})
   422  
   423  	// make sure we're starting with a fresh cache
   424  	s.execDataHeroCache.Clear()
   425  
   426  	s.Run("returns error for unindexed start height", func() {
   427  		subCtx, subCancel := context.WithCancel(ctx)
   428  		defer subCancel()
   429  
   430  		sub := s.backend.SubscribeExecutionData(subCtx, flow.ZeroID, s.blocks[len(s.blocks)-1].Header.Height+10)
   431  		assert.Equal(s.T(), codes.NotFound, status.Code(sub.Err()))
   432  	})
   433  }
   434  
   435  func (s *BackendExecutionDataSuite) TestGetRegisterValues() {
   436  	s.Run("normal case", func() {
   437  		res, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, s.backend.rootBlockHeight)
   438  		require.NoError(s.T(), err)
   439  		require.NotEmpty(s.T(), res)
   440  	})
   441  
   442  	s.Run("returns error if block height is out of range", func() {
   443  		res, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, s.backend.rootBlockHeight+1)
   444  		require.Nil(s.T(), res)
   445  		require.Equal(s.T(), codes.OutOfRange, status.Code(err))
   446  	})
   447  
   448  	s.Run("returns error if register path is not indexed", func() {
   449  		falseID := flow.RegisterIDs{flow.RegisterID{Owner: "ha", Key: "ha"}}
   450  		res, err := s.backend.GetRegisterValues(falseID, s.backend.rootBlockHeight)
   451  		require.Nil(s.T(), res)
   452  		require.Equal(s.T(), codes.NotFound, status.Code(err))
   453  	})
   454  
   455  	s.Run("returns error if too many registers are requested", func() {
   456  		res, err := s.backend.GetRegisterValues(make(flow.RegisterIDs, s.backend.registerRequestLimit+1), s.backend.rootBlockHeight)
   457  		require.Nil(s.T(), res)
   458  		require.Equal(s.T(), codes.InvalidArgument, status.Code(err))
   459  	})
   460  }