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 }