github.com/onflow/flow-go@v0.33.17/engine/access/state_stream/backend/handler_test.go (about) 1 package backend 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "sync" 8 "testing" 9 "time" 10 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 pb "google.golang.org/genproto/googleapis/bytestream" 16 "google.golang.org/grpc" 17 "google.golang.org/grpc/codes" 18 "google.golang.org/grpc/status" 19 20 "github.com/onflow/cadence/encoding/ccf" 21 jsoncdc "github.com/onflow/cadence/encoding/json" 22 "github.com/onflow/flow/protobuf/go/flow/entities" 23 "github.com/onflow/flow/protobuf/go/flow/executiondata" 24 25 "github.com/onflow/flow-go/engine/access/state_stream" 26 ssmock "github.com/onflow/flow-go/engine/access/state_stream/mock" 27 "github.com/onflow/flow-go/engine/common/rpc/convert" 28 "github.com/onflow/flow-go/model/flow" 29 "github.com/onflow/flow-go/storage" 30 "github.com/onflow/flow-go/utils/unittest" 31 "github.com/onflow/flow-go/utils/unittest/generator" 32 ) 33 34 func TestHeartbeatResponseSuite(t *testing.T) { 35 suite.Run(t, new(HandlerTestSuite)) 36 } 37 38 type HandlerTestSuite struct { 39 BackendExecutionDataSuite 40 handler *Handler 41 } 42 43 // fakeReadServerImpl is an utility structure for receiving response from grpc handler without building a complete pipeline with client and server. 44 // It allows to receive streamed events pushed by server in buffered channel that can be later used to assert correctness of responses 45 type fakeReadServerImpl struct { 46 pb.ByteStream_ReadServer 47 ctx context.Context 48 received chan *executiondata.SubscribeEventsResponse 49 } 50 51 var _ executiondata.ExecutionDataAPI_SubscribeEventsServer = (*fakeReadServerImpl)(nil) 52 53 func (fake *fakeReadServerImpl) Context() context.Context { 54 return fake.ctx 55 } 56 57 func (fake *fakeReadServerImpl) Send(response *executiondata.SubscribeEventsResponse) error { 58 fake.received <- response 59 return nil 60 } 61 62 func (s *HandlerTestSuite) SetupTest() { 63 s.BackendExecutionDataSuite.SetupTest() 64 chain := flow.MonotonicEmulator.Chain() 65 s.handler = NewHandler(s.backend, chain, makeConfig(5)) 66 } 67 68 // TestHeartbeatResponse tests the periodic heartbeat response. 69 // 70 // Test Steps: 71 // - Generate different events in blocks. 72 // - Create different filters for generated events. 73 // - Wait for either responses with filtered events or heartbeat responses. 74 // - Verify that the responses are being sent with proper heartbeat interval. 75 func (s *HandlerTestSuite) TestHeartbeatResponse() { 76 reader := &fakeReadServerImpl{ 77 ctx: context.Background(), 78 received: make(chan *executiondata.SubscribeEventsResponse, 100), 79 } 80 81 // notify backend block is available 82 s.backend.setHighestHeight(s.blocks[len(s.blocks)-1].Header.Height) 83 84 s.Run("All events filter", func() { 85 // create empty event filter 86 filter := &executiondata.EventFilter{} 87 // create subscribe events request, set the created filter and heartbeatInterval 88 req := &executiondata.SubscribeEventsRequest{ 89 StartBlockHeight: 0, 90 Filter: filter, 91 HeartbeatInterval: 1, 92 } 93 94 // subscribe for events 95 go func() { 96 err := s.handler.SubscribeEvents(req, reader) 97 require.NoError(s.T(), err) 98 }() 99 100 for _, b := range s.blocks { 101 // consume execution data from subscription 102 unittest.RequireReturnsBefore(s.T(), func() { 103 resp, ok := <-reader.received 104 require.True(s.T(), ok, "channel closed while waiting for exec data for block %d %v", b.Header.Height, b.ID()) 105 106 blockID, err := convert.BlockID(resp.BlockId) 107 require.NoError(s.T(), err) 108 require.Equal(s.T(), b.Header.ID(), blockID) 109 require.Equal(s.T(), b.Header.Height, resp.BlockHeight) 110 }, time.Second, fmt.Sprintf("timed out waiting for exec data for block %d %v", b.Header.Height, b.ID())) 111 } 112 }) 113 114 s.Run("Event A.0x1.Foo.Bar filter with heartbeat interval 1", func() { 115 // create A.0x1.Foo.Bar event filter 116 pbFilter := &executiondata.EventFilter{ 117 EventType: []string{string(testEventTypes[0])}, 118 Contract: nil, 119 Address: nil, 120 } 121 // create subscribe events request, set the created filter and heartbeatInterval 122 req := &executiondata.SubscribeEventsRequest{ 123 StartBlockHeight: 0, 124 Filter: pbFilter, 125 HeartbeatInterval: 1, 126 } 127 128 // subscribe for events 129 go func() { 130 err := s.handler.SubscribeEvents(req, reader) 131 require.NoError(s.T(), err) 132 }() 133 134 for _, b := range s.blocks { 135 136 // consume execution data from subscription 137 unittest.RequireReturnsBefore(s.T(), func() { 138 resp, ok := <-reader.received 139 require.True(s.T(), ok, "channel closed while waiting for exec data for block %d %v", b.Header.Height, b.ID()) 140 141 blockID, err := convert.BlockID(resp.BlockId) 142 require.NoError(s.T(), err) 143 require.Equal(s.T(), b.Header.ID(), blockID) 144 require.Equal(s.T(), b.Header.Height, resp.BlockHeight) 145 }, time.Second, fmt.Sprintf("timed out waiting for exec data for block %d %v", b.Header.Height, b.ID())) 146 } 147 }) 148 149 s.Run("Non existent filter with heartbeat interval 2", func() { 150 // create non existent filter 151 pbFilter := &executiondata.EventFilter{ 152 EventType: []string{"A.0x1.NonExistent.Event"}, 153 Contract: nil, 154 Address: nil, 155 } 156 157 // create subscribe events request, set the created filter and heartbeatInterval 158 req := &executiondata.SubscribeEventsRequest{ 159 StartBlockHeight: 0, 160 Filter: pbFilter, 161 HeartbeatInterval: 2, 162 } 163 164 // subscribe for events 165 go func() { 166 err := s.handler.SubscribeEvents(req, reader) 167 require.NoError(s.T(), err) 168 }() 169 170 // expect a response for every other block 171 expectedBlocks := make([]*flow.Block, 0) 172 for i, block := range s.blocks { 173 if (i+1)%int(req.HeartbeatInterval) == 0 { 174 expectedBlocks = append(expectedBlocks, block) 175 } 176 } 177 178 require.Len(s.T(), expectedBlocks, len(s.blocks)/int(req.HeartbeatInterval)) 179 180 for _, b := range expectedBlocks { 181 // consume execution data from subscription 182 unittest.RequireReturnsBefore(s.T(), func() { 183 resp, ok := <-reader.received 184 require.True(s.T(), ok, "channel closed while waiting for exec data for block %d %v", b.Header.Height, b.ID()) 185 186 blockID, err := convert.BlockID(resp.BlockId) 187 require.NoError(s.T(), err) 188 require.Equal(s.T(), b.Header.Height, resp.BlockHeight) 189 require.Equal(s.T(), b.Header.ID(), blockID) 190 require.Empty(s.T(), resp.Events) 191 }, time.Second, fmt.Sprintf("timed out waiting for exec data for block %d %v", b.Header.Height, b.ID())) 192 } 193 }) 194 } 195 196 // TestGetExecutionDataByBlockID tests the execution data by block id with different event encoding versions. 197 func TestGetExecutionDataByBlockID(t *testing.T) { 198 ctx, cancel := context.WithCancel(context.Background()) 199 defer cancel() 200 201 ccfEvents, jsonEvents := generateEvents(t, 3) 202 203 tests := []struct { 204 eventVersion entities.EventEncodingVersion 205 expected []flow.Event 206 }{ 207 { 208 entities.EventEncodingVersion_JSON_CDC_V0, 209 jsonEvents, 210 }, 211 { 212 entities.EventEncodingVersion_CCF_V0, 213 ccfEvents, 214 }, 215 } 216 217 for _, test := range tests { 218 t.Run(fmt.Sprintf("test %s event encoding version", test.eventVersion.String()), func(t *testing.T) { 219 result := unittest.BlockExecutionDataFixture( 220 unittest.WithChunkExecutionDatas( 221 unittest.ChunkExecutionDataFixture(t, 1024, unittest.WithChunkEvents(ccfEvents)), 222 unittest.ChunkExecutionDataFixture(t, 1024, unittest.WithChunkEvents(ccfEvents)), 223 ), 224 ) 225 blockID := result.BlockID 226 227 api := ssmock.NewAPI(t) 228 api.On("GetExecutionDataByBlockID", mock.Anything, blockID).Return(result, nil) 229 230 h := NewHandler(api, flow.Localnet.Chain(), makeConfig(1)) 231 232 response, err := h.GetExecutionDataByBlockID(ctx, &executiondata.GetExecutionDataByBlockIDRequest{ 233 BlockId: blockID[:], 234 EventEncodingVersion: test.eventVersion, 235 }) 236 require.NoError(t, err) 237 require.NotNil(t, response) 238 239 blockExecutionData := response.GetBlockExecutionData() 240 require.Equal(t, blockID[:], blockExecutionData.GetBlockId()) 241 242 convertedExecData, err := convert.MessageToBlockExecutionData(blockExecutionData, flow.Testnet.Chain()) 243 require.NoError(t, err) 244 245 // Verify that the payload is valid 246 for _, chunk := range convertedExecData.ChunkExecutionDatas { 247 for i, e := range chunk.Events { 248 assert.Equal(t, test.expected[i], e) 249 250 var err error 251 if test.eventVersion == entities.EventEncodingVersion_JSON_CDC_V0 { 252 _, err = jsoncdc.Decode(nil, e.Payload) 253 } else { 254 _, err = ccf.Decode(nil, e.Payload) 255 } 256 require.NoError(t, err) 257 } 258 } 259 }) 260 } 261 } 262 263 // TestExecutionDataStream tests the execution data stream with different event encoding versions. 264 func TestExecutionDataStream(t *testing.T) { 265 t.Parallel() 266 267 ctx, cancel := context.WithCancel(context.Background()) 268 defer cancel() 269 270 // Send a single response. 271 blockHeight := uint64(1) 272 273 // Helper function to perform a stream request and handle responses. 274 makeStreamRequest := func( 275 stream *StreamMock[executiondata.SubscribeExecutionDataRequest, executiondata.SubscribeExecutionDataResponse], 276 api *ssmock.API, 277 request *executiondata.SubscribeExecutionDataRequest, 278 response *ExecutionDataResponse, 279 ) { 280 sub := NewSubscription(1) 281 282 api.On("SubscribeExecutionData", mock.Anything, flow.ZeroID, uint64(0), mock.Anything).Return(sub) 283 284 h := NewHandler(api, flow.Localnet.Chain(), makeConfig(1)) 285 286 wg := sync.WaitGroup{} 287 wg.Add(1) 288 go func() { 289 wg.Done() 290 err := h.SubscribeExecutionData(request, stream) 291 require.NoError(t, err) 292 t.Log("subscription closed") 293 }() 294 wg.Wait() 295 296 err := sub.Send(ctx, response, 100*time.Millisecond) 297 require.NoError(t, err) 298 299 // Notify end of data. 300 sub.Close() 301 } 302 303 // handleExecutionDataStreamResponses handles responses from the execution data stream. 304 handleExecutionDataStreamResponses := func( 305 stream *StreamMock[executiondata.SubscribeExecutionDataRequest, executiondata.SubscribeExecutionDataResponse], 306 version entities.EventEncodingVersion, 307 expectedEvents []flow.Event, 308 ) { 309 var responses []*executiondata.SubscribeExecutionDataResponse 310 for { 311 t.Log(len(responses)) 312 resp, err := stream.RecvToClient() 313 if err == io.EOF { 314 break 315 } 316 require.NoError(t, err) 317 responses = append(responses, resp) 318 close(stream.sentFromServer) 319 } 320 321 for _, resp := range responses { 322 convertedExecData, err := convert.MessageToBlockExecutionData(resp.GetBlockExecutionData(), flow.Testnet.Chain()) 323 require.NoError(t, err) 324 325 assert.Equal(t, blockHeight, resp.GetBlockHeight()) 326 327 // only expect a single response 328 assert.Equal(t, 1, len(responses)) 329 330 // Verify that the payload is valid 331 for _, chunk := range convertedExecData.ChunkExecutionDatas { 332 for i, e := range chunk.Events { 333 assert.Equal(t, expectedEvents[i], e) 334 335 var err error 336 if version == entities.EventEncodingVersion_JSON_CDC_V0 { 337 _, err = jsoncdc.Decode(nil, e.Payload) 338 } else { 339 _, err = ccf.Decode(nil, e.Payload) 340 } 341 require.NoError(t, err) 342 } 343 } 344 } 345 } 346 347 ccfEvents, jsonEvents := generateEvents(t, 3) 348 349 tests := []struct { 350 eventVersion entities.EventEncodingVersion 351 expected []flow.Event 352 }{ 353 { 354 entities.EventEncodingVersion_JSON_CDC_V0, 355 jsonEvents, 356 }, 357 { 358 entities.EventEncodingVersion_CCF_V0, 359 ccfEvents, 360 }, 361 } 362 363 for _, test := range tests { 364 t.Run(fmt.Sprintf("test %s event encoding version", test.eventVersion.String()), func(t *testing.T) { 365 api := ssmock.NewAPI(t) 366 stream := makeStreamMock[executiondata.SubscribeExecutionDataRequest, executiondata.SubscribeExecutionDataResponse](ctx) 367 368 makeStreamRequest( 369 stream, 370 api, 371 &executiondata.SubscribeExecutionDataRequest{ 372 EventEncodingVersion: test.eventVersion, 373 }, 374 &ExecutionDataResponse{ 375 Height: blockHeight, 376 ExecutionData: unittest.BlockExecutionDataFixture( 377 unittest.WithChunkExecutionDatas( 378 unittest.ChunkExecutionDataFixture(t, 1024, unittest.WithChunkEvents(ccfEvents)), 379 unittest.ChunkExecutionDataFixture(t, 1024, unittest.WithChunkEvents(ccfEvents)), 380 ), 381 ), 382 }, 383 ) 384 handleExecutionDataStreamResponses(stream, test.eventVersion, test.expected) 385 }) 386 } 387 } 388 389 // TestEventStream tests the event stream with different event encoding versions. 390 func TestEventStream(t *testing.T) { 391 t.Parallel() 392 393 ctx, cancel := context.WithCancel(context.Background()) 394 defer cancel() 395 396 blockHeight := uint64(1) 397 blockID := unittest.IdentifierFixture() 398 399 // Helper function to perform a stream request and handle responses. 400 makeStreamRequest := func( 401 stream *StreamMock[executiondata.SubscribeEventsRequest, executiondata.SubscribeEventsResponse], 402 api *ssmock.API, 403 request *executiondata.SubscribeEventsRequest, 404 response *EventsResponse, 405 ) { 406 sub := NewSubscription(1) 407 408 api.On("SubscribeEvents", mock.Anything, flow.ZeroID, uint64(0), mock.Anything).Return(sub) 409 410 h := NewHandler(api, flow.Localnet.Chain(), makeConfig(1)) 411 412 wg := sync.WaitGroup{} 413 wg.Add(1) 414 go func() { 415 wg.Done() 416 err := h.SubscribeEvents(request, stream) 417 require.NoError(t, err) 418 t.Log("subscription closed") 419 }() 420 wg.Wait() 421 422 // send a single response 423 err := sub.Send(ctx, response, 100*time.Millisecond) 424 require.NoError(t, err) 425 426 // notify end of data 427 sub.Close() 428 } 429 430 // handleExecutionDataStreamResponses handles responses from the execution data stream. 431 handleExecutionDataStreamResponses := func( 432 stream *StreamMock[executiondata.SubscribeEventsRequest, executiondata.SubscribeEventsResponse], 433 version entities.EventEncodingVersion, 434 expectedEvents []flow.Event, 435 ) { 436 var responses []*executiondata.SubscribeEventsResponse 437 for { 438 t.Log(len(responses)) 439 resp, err := stream.RecvToClient() 440 if err == io.EOF { 441 break 442 } 443 // make sure the payload is valid 444 require.NoError(t, err) 445 responses = append(responses, resp) 446 447 // shutdown the stream after one response 448 close(stream.sentFromServer) 449 } 450 451 for _, resp := range responses { 452 convertedEvents := convert.MessagesToEvents(resp.GetEvents()) 453 454 assert.Equal(t, blockHeight, resp.GetBlockHeight()) 455 assert.Equal(t, blockID, convert.MessageToIdentifier(resp.GetBlockId())) 456 assert.Equal(t, expectedEvents, convertedEvents) 457 // only expect a single response 458 assert.Equal(t, 1, len(responses)) 459 460 for _, e := range convertedEvents { 461 var err error 462 if version == entities.EventEncodingVersion_JSON_CDC_V0 { 463 _, err = jsoncdc.Decode(nil, e.Payload) 464 } else { 465 _, err = ccf.Decode(nil, e.Payload) 466 } 467 require.NoError(t, err) 468 } 469 } 470 } 471 472 // generate events with a payload to include 473 ccfEvents, jsonEvents := generateEvents(t, 3) 474 475 tests := []struct { 476 eventVersion entities.EventEncodingVersion 477 expected []flow.Event 478 }{ 479 { 480 entities.EventEncodingVersion_JSON_CDC_V0, 481 jsonEvents, 482 }, 483 { 484 entities.EventEncodingVersion_CCF_V0, 485 ccfEvents, 486 }, 487 } 488 489 for _, test := range tests { 490 t.Run(fmt.Sprintf("test %s event encoding version", test.eventVersion.String()), func(t *testing.T) { 491 stream := makeStreamMock[executiondata.SubscribeEventsRequest, executiondata.SubscribeEventsResponse](ctx) 492 493 makeStreamRequest( 494 stream, 495 ssmock.NewAPI(t), 496 &executiondata.SubscribeEventsRequest{ 497 EventEncodingVersion: test.eventVersion, 498 }, 499 &EventsResponse{ 500 BlockID: blockID, 501 Height: blockHeight, 502 Events: ccfEvents, 503 }, 504 ) 505 handleExecutionDataStreamResponses(stream, test.eventVersion, test.expected) 506 }) 507 } 508 } 509 510 // TestGetRegisterValues tests the register values. 511 func TestGetRegisterValues(t *testing.T) { 512 t.Parallel() 513 514 ctx, cancel := context.WithCancel(context.Background()) 515 defer cancel() 516 517 testHeight := uint64(1) 518 519 // test register IDs + values 520 testIds := flow.RegisterIDs{ 521 flow.UUIDRegisterID(0), 522 flow.AccountStatusRegisterID(unittest.AddressFixture()), 523 unittest.RegisterIDFixture(), 524 } 525 testValues := []flow.RegisterValue{ 526 []byte("uno"), 527 []byte("dos"), 528 []byte("tres"), 529 } 530 invalidIDs := append(testIds, flow.RegisterID{}) // valid + invalid IDs 531 532 t.Run("invalid message", func(t *testing.T) { 533 api := ssmock.NewAPI(t) 534 h := NewHandler(api, flow.Testnet.Chain(), makeConfig(1)) 535 536 invalidMessage := &executiondata.GetRegisterValuesRequest{ 537 RegisterIds: nil, 538 } 539 _, err := h.GetRegisterValues(ctx, invalidMessage) 540 require.Equal(t, codes.InvalidArgument, status.Code(err)) 541 }) 542 543 t.Run("valid registers", func(t *testing.T) { 544 api := ssmock.NewAPI(t) 545 api.On("GetRegisterValues", testIds, testHeight).Return(testValues, nil) 546 h := NewHandler(api, flow.Testnet.Chain(), makeConfig(1)) 547 548 validRegisters := make([]*entities.RegisterID, len(testIds)) 549 for i, id := range testIds { 550 validRegisters[i] = convert.RegisterIDToMessage(id) 551 } 552 553 req := &executiondata.GetRegisterValuesRequest{ 554 RegisterIds: validRegisters, 555 BlockHeight: testHeight, 556 } 557 558 resp, err := h.GetRegisterValues(ctx, req) 559 require.NoError(t, err) 560 require.Equal(t, testValues, resp.GetValues()) 561 }) 562 563 t.Run("unavailable registers", func(t *testing.T) { 564 api := ssmock.NewAPI(t) 565 expectedErr := status.Errorf(codes.NotFound, "could not get register values: %v", storage.ErrNotFound) 566 api.On("GetRegisterValues", invalidIDs, testHeight).Return(nil, expectedErr) 567 h := NewHandler(api, flow.Testnet.Chain(), makeConfig(1)) 568 569 unavailableRegisters := make([]*entities.RegisterID, len(invalidIDs)) 570 for i, id := range invalidIDs { 571 unavailableRegisters[i] = convert.RegisterIDToMessage(id) 572 } 573 574 req := &executiondata.GetRegisterValuesRequest{ 575 RegisterIds: unavailableRegisters, 576 BlockHeight: testHeight, 577 } 578 579 _, err := h.GetRegisterValues(ctx, req) 580 require.Equal(t, codes.NotFound, status.Code(err)) 581 }) 582 583 t.Run("wrong height", func(t *testing.T) { 584 api := ssmock.NewAPI(t) 585 expectedErr := status.Errorf(codes.OutOfRange, "could not get register values: %v", storage.ErrHeightNotIndexed) 586 api.On("GetRegisterValues", testIds, testHeight+1).Return(nil, expectedErr) 587 h := NewHandler(api, flow.Testnet.Chain(), makeConfig(1)) 588 589 validRegisters := make([]*entities.RegisterID, len(testIds)) 590 for i, id := range testIds { 591 validRegisters[i] = convert.RegisterIDToMessage(id) 592 } 593 594 req := &executiondata.GetRegisterValuesRequest{ 595 RegisterIds: validRegisters, 596 BlockHeight: testHeight + 1, 597 } 598 599 _, err := h.GetRegisterValues(ctx, req) 600 require.Equal(t, codes.OutOfRange, status.Code(err)) 601 }) 602 } 603 604 func generateEvents(t *testing.T, n int) ([]flow.Event, []flow.Event) { 605 ccfEvents := generator.GetEventsWithEncoding(n, entities.EventEncodingVersion_CCF_V0) 606 jsonEvents := make([]flow.Event, len(ccfEvents)) 607 for i, e := range ccfEvents { 608 jsonEvent, err := convert.CcfEventToJsonEvent(e) 609 require.NoError(t, err) 610 jsonEvents[i] = *jsonEvent 611 } 612 return ccfEvents, jsonEvents 613 } 614 615 func makeConfig(maxGlobalStreams uint32) Config { 616 return Config{ 617 EventFilterConfig: state_stream.DefaultEventFilterConfig, 618 ClientSendTimeout: state_stream.DefaultSendTimeout, 619 ClientSendBufferSize: state_stream.DefaultSendBufferSize, 620 MaxGlobalStreams: maxGlobalStreams, 621 HeartbeatInterval: state_stream.DefaultHeartbeatInterval, 622 } 623 } 624 625 func makeStreamMock[R, T any](ctx context.Context) *StreamMock[R, T] { 626 return &StreamMock[R, T]{ 627 ctx: ctx, 628 recvToServer: make(chan *R, 10), 629 sentFromServer: make(chan *T, 10), 630 } 631 } 632 633 type StreamMock[R, T any] struct { 634 grpc.ServerStream 635 ctx context.Context 636 recvToServer chan *R 637 sentFromServer chan *T 638 } 639 640 func (m *StreamMock[R, T]) Context() context.Context { 641 return m.ctx 642 } 643 func (m *StreamMock[R, T]) Send(resp *T) error { 644 m.sentFromServer <- resp 645 return nil 646 } 647 648 func (m *StreamMock[R, T]) Recv() (*R, error) { 649 req, more := <-m.recvToServer 650 if !more { 651 return nil, io.EOF 652 } 653 return req, nil 654 } 655 656 func (m *StreamMock[R, T]) SendFromClient(req *R) error { 657 m.recvToServer <- req 658 return nil 659 } 660 661 func (m *StreamMock[R, T]) RecvToClient() (*T, error) { 662 response, more := <-m.sentFromServer 663 if !more { 664 return nil, io.EOF 665 } 666 return response, nil 667 }