github.com/onflow/flow-go@v0.33.17/engine/access/rpc/backend/backend_events_test.go (about) 1 package backend 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "sort" 8 "testing" 9 10 "github.com/rs/zerolog" 11 "github.com/stretchr/testify/mock" 12 "github.com/stretchr/testify/suite" 13 "google.golang.org/grpc/codes" 14 "google.golang.org/grpc/status" 15 16 "github.com/onflow/cadence/encoding/ccf" 17 jsoncdc "github.com/onflow/cadence/encoding/json" 18 "github.com/onflow/flow/protobuf/go/flow/entities" 19 execproto "github.com/onflow/flow/protobuf/go/flow/execution" 20 21 access "github.com/onflow/flow-go/engine/access/mock" 22 connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock" 23 "github.com/onflow/flow-go/engine/common/rpc/convert" 24 "github.com/onflow/flow-go/model/flow" 25 "github.com/onflow/flow-go/module/irrecoverable" 26 syncmock "github.com/onflow/flow-go/module/state_synchronization/mock" 27 protocol "github.com/onflow/flow-go/state/protocol/mock" 28 "github.com/onflow/flow-go/storage" 29 storagemock "github.com/onflow/flow-go/storage/mock" 30 "github.com/onflow/flow-go/utils/unittest" 31 "github.com/onflow/flow-go/utils/unittest/generator" 32 ) 33 34 var targetEvent string 35 36 type testCase struct { 37 encoding entities.EventEncodingVersion 38 queryMode IndexQueryMode 39 } 40 41 type BackendEventsSuite struct { 42 suite.Suite 43 44 log zerolog.Logger 45 state *protocol.State 46 snapshot *protocol.Snapshot 47 params *protocol.Params 48 rootHeader *flow.Header 49 50 eventsIndex *EventsIndex 51 events *storagemock.Events 52 headers *storagemock.Headers 53 receipts *storagemock.ExecutionReceipts 54 connectionFactory *connectionmock.ConnectionFactory 55 chainID flow.ChainID 56 57 executionNodes flow.IdentityList 58 execClient *access.ExecutionAPIClient 59 60 sealedHead *flow.Header 61 blocks []*flow.Block 62 blockIDs []flow.Identifier 63 blockEvents []flow.Event 64 65 testCases []testCase 66 } 67 68 func TestBackendEventsSuite(t *testing.T) { 69 suite.Run(t, new(BackendEventsSuite)) 70 } 71 72 func (s *BackendEventsSuite) SetupTest() { 73 s.log = unittest.Logger() 74 s.state = protocol.NewState(s.T()) 75 s.snapshot = protocol.NewSnapshot(s.T()) 76 s.rootHeader = unittest.BlockHeaderFixture() 77 s.params = protocol.NewParams(s.T()) 78 s.events = storagemock.NewEvents(s.T()) 79 s.headers = storagemock.NewHeaders(s.T()) 80 s.receipts = storagemock.NewExecutionReceipts(s.T()) 81 s.connectionFactory = connectionmock.NewConnectionFactory(s.T()) 82 s.chainID = flow.Testnet 83 84 s.execClient = access.NewExecutionAPIClient(s.T()) 85 s.executionNodes = unittest.IdentityListFixture(2, unittest.WithRole(flow.RoleExecution)) 86 s.eventsIndex = NewEventsIndex(s.events) 87 88 blockCount := 5 89 s.blocks = make([]*flow.Block, blockCount) 90 s.blockIDs = make([]flow.Identifier, blockCount) 91 92 for i := 0; i < blockCount; i++ { 93 var header *flow.Header 94 if i == 0 { 95 header = unittest.BlockHeaderFixture() 96 } else { 97 header = unittest.BlockHeaderWithParentFixture(s.blocks[i-1].Header) 98 } 99 100 payload := unittest.PayloadFixture() 101 header.PayloadHash = payload.Hash() 102 block := &flow.Block{ 103 Header: header, 104 Payload: &payload, 105 } 106 // the last block is sealed 107 if i == blockCount-1 { 108 s.sealedHead = header 109 } 110 111 s.blocks[i] = block 112 s.blockIDs[i] = block.ID() 113 114 s.T().Logf("block %d: %s", header.Height, block.ID()) 115 } 116 117 s.blockEvents = generator.GetEventsWithEncoding(10, entities.EventEncodingVersion_CCF_V0) 118 targetEvent = string(s.blockEvents[0].Type) 119 120 // events returned from the db are sorted by txID, txIndex, then eventIndex. 121 // reproduce that here to ensure output order works as expected 122 returnBlockEvents := make([]flow.Event, len(s.blockEvents)) 123 copy(returnBlockEvents, s.blockEvents) 124 125 sort.Slice(returnBlockEvents, func(i, j int) bool { 126 return bytes.Compare(returnBlockEvents[i].TransactionID[:], returnBlockEvents[j].TransactionID[:]) < 0 127 }) 128 129 s.events.On("ByBlockID", mock.Anything).Return(func(blockID flow.Identifier) ([]flow.Event, error) { 130 for _, headerID := range s.blockIDs { 131 if blockID == headerID { 132 return returnBlockEvents, nil 133 } 134 } 135 return nil, storage.ErrNotFound 136 }).Maybe() 137 138 s.headers.On("BlockIDByHeight", mock.Anything).Return(func(height uint64) (flow.Identifier, error) { 139 for _, block := range s.blocks { 140 if height == block.Header.Height { 141 return block.ID(), nil 142 } 143 } 144 return flow.ZeroID, storage.ErrNotFound 145 }).Maybe() 146 147 s.headers.On("ByBlockID", mock.Anything).Return(func(blockID flow.Identifier) (*flow.Header, error) { 148 for _, block := range s.blocks { 149 if blockID == block.ID() { 150 return block.Header, nil 151 } 152 } 153 return nil, storage.ErrNotFound 154 }).Maybe() 155 156 s.testCases = make([]testCase, 0) 157 158 for _, encoding := range []entities.EventEncodingVersion{ 159 entities.EventEncodingVersion_CCF_V0, 160 entities.EventEncodingVersion_JSON_CDC_V0, 161 } { 162 for _, queryMode := range []IndexQueryMode{ 163 IndexQueryModeExecutionNodesOnly, 164 IndexQueryModeLocalOnly, 165 IndexQueryModeFailover, 166 } { 167 s.testCases = append(s.testCases, testCase{ 168 encoding: encoding, 169 queryMode: queryMode, 170 }) 171 } 172 } 173 } 174 175 func (s *BackendEventsSuite) defaultBackend() *backendEvents { 176 return &backendEvents{ 177 log: s.log, 178 chain: s.chainID.Chain(), 179 state: s.state, 180 headers: s.headers, 181 executionReceipts: s.receipts, 182 connFactory: s.connectionFactory, 183 nodeCommunicator: NewNodeCommunicator(false), 184 maxHeightRange: DefaultMaxHeightRange, 185 queryMode: IndexQueryModeExecutionNodesOnly, 186 eventsIndex: s.eventsIndex, 187 } 188 } 189 190 // setupExecutionNodes sets up the mocks required to test against an EN backend 191 func (s *BackendEventsSuite) setupExecutionNodes(block *flow.Block) { 192 s.params.On("FinalizedRoot").Return(s.rootHeader, nil) 193 s.state.On("Params").Return(s.params) 194 s.state.On("Final").Return(s.snapshot) 195 s.snapshot.On("Identities", mock.Anything).Return(s.executionNodes, nil) 196 197 // this line causes a S1021 lint error because receipts is explicitly declared. this is required 198 // to ensure the mock library handles the response type correctly 199 var receipts flow.ExecutionReceiptList //nolint:gosimple 200 receipts = unittest.ReceiptsForBlockFixture(block, s.executionNodes.NodeIDs()) 201 s.receipts.On("ByBlockID", block.ID()).Return(receipts, nil) 202 203 s.connectionFactory.On("GetExecutionAPIClient", mock.Anything). 204 Return(s.execClient, &mockCloser{}, nil) 205 } 206 207 // setupENSuccessResponse configures the execution node client to return a successful response 208 func (s *BackendEventsSuite) setupENSuccessResponse(eventType string, blocks []*flow.Block) { 209 s.setupExecutionNodes(blocks[len(blocks)-1]) 210 211 ids := make([][]byte, len(blocks)) 212 results := make([]*execproto.GetEventsForBlockIDsResponse_Result, len(blocks)) 213 214 events := make([]*entities.Event, 0) 215 for _, event := range s.blockEvents { 216 if string(event.Type) == eventType { 217 events = append(events, convert.EventToMessage(event)) 218 } 219 } 220 221 for i, block := range blocks { 222 id := block.ID() 223 ids[i] = id[:] 224 results[i] = &execproto.GetEventsForBlockIDsResponse_Result{ 225 BlockId: id[:], 226 BlockHeight: block.Header.Height, 227 Events: events, 228 } 229 } 230 expectedExecRequest := &execproto.GetEventsForBlockIDsRequest{ 231 Type: eventType, 232 BlockIds: ids, 233 } 234 expectedResponse := &execproto.GetEventsForBlockIDsResponse{ 235 Results: results, 236 EventEncodingVersion: entities.EventEncodingVersion_CCF_V0, 237 } 238 239 s.execClient.On("GetEventsForBlockIDs", mock.Anything, expectedExecRequest). 240 Return(expectedResponse, nil) 241 } 242 243 // setupENFailingResponse configures the execution node client to return an error 244 func (s *BackendEventsSuite) setupENFailingResponse(eventType string, headers []*flow.Header, err error) { 245 ids := make([][]byte, len(headers)) 246 for i, header := range headers { 247 id := header.ID() 248 ids[i] = id[:] 249 } 250 failingRequest := &execproto.GetEventsForBlockIDsRequest{ 251 Type: eventType, 252 BlockIds: ids, 253 } 254 255 s.execClient.On("GetEventsForBlockIDs", mock.Anything, failingRequest). 256 Return(nil, err) 257 } 258 259 // TestGetEvents_HappyPaths tests the happy paths for GetEventsForBlockIDs and GetEventsForHeightRange 260 // across all queryModes and encodings 261 func (s *BackendEventsSuite) TestGetEvents_HappyPaths() { 262 ctx := context.Background() 263 264 startHeight := s.blocks[0].Header.Height 265 endHeight := s.sealedHead.Height 266 267 reporter := syncmock.NewIndexReporter(s.T()) 268 reporter.On("LowestIndexedHeight").Return(startHeight, nil) 269 reporter.On("HighestIndexedHeight").Return(endHeight+10, nil) 270 err := s.eventsIndex.Initialize(reporter) 271 s.Require().NoError(err) 272 273 s.state.On("Sealed").Return(s.snapshot) 274 s.snapshot.On("Head").Return(s.sealedHead, nil) 275 276 s.Run("GetEventsForHeightRange - end height updated", func() { 277 backend := s.defaultBackend() 278 backend.queryMode = IndexQueryModeFailover 279 endHeight := startHeight + 20 // should still return 5 responses 280 encoding := entities.EventEncodingVersion_CCF_V0 281 282 response, err := backend.GetEventsForHeightRange(ctx, targetEvent, startHeight, endHeight, encoding) 283 s.Require().NoError(err) 284 285 s.assertResponse(response, encoding) 286 }) 287 288 for _, tt := range s.testCases { 289 s.Run(fmt.Sprintf("all from storage - %s - %s", tt.encoding.String(), tt.queryMode), func() { 290 switch tt.queryMode { 291 case IndexQueryModeExecutionNodesOnly: 292 // not applicable 293 return 294 case IndexQueryModeLocalOnly, IndexQueryModeFailover: 295 // only calls to local storage 296 } 297 298 backend := s.defaultBackend() 299 backend.queryMode = tt.queryMode 300 301 response, err := backend.GetEventsForBlockIDs(ctx, targetEvent, s.blockIDs, tt.encoding) 302 s.Require().NoError(err) 303 s.assertResponse(response, tt.encoding) 304 305 response, err = backend.GetEventsForHeightRange(ctx, targetEvent, startHeight, endHeight, tt.encoding) 306 s.Require().NoError(err) 307 s.assertResponse(response, tt.encoding) 308 }) 309 310 s.Run(fmt.Sprintf("all from en - %s - %s", tt.encoding.String(), tt.queryMode), func() { 311 events := storagemock.NewEvents(s.T()) 312 eventsIndex := NewEventsIndex(events) 313 314 switch tt.queryMode { 315 case IndexQueryModeLocalOnly: 316 // not applicable 317 return 318 case IndexQueryModeExecutionNodesOnly: 319 // only calls to EN, no calls to storage 320 case IndexQueryModeFailover: 321 // all calls to storage fail 322 // simulated by not initializing the eventIndex so all calls return ErrIndexNotInitialized 323 } 324 325 backend := s.defaultBackend() 326 backend.queryMode = tt.queryMode 327 backend.eventsIndex = eventsIndex 328 329 s.setupENSuccessResponse(targetEvent, s.blocks) 330 331 response, err := backend.GetEventsForBlockIDs(ctx, targetEvent, s.blockIDs, tt.encoding) 332 s.Require().NoError(err) 333 s.assertResponse(response, tt.encoding) 334 335 response, err = backend.GetEventsForHeightRange(ctx, targetEvent, startHeight, endHeight, tt.encoding) 336 s.Require().NoError(err) 337 s.assertResponse(response, tt.encoding) 338 }) 339 340 s.Run(fmt.Sprintf("mixed storage & en - %s - %s", tt.encoding.String(), tt.queryMode), func() { 341 events := storagemock.NewEvents(s.T()) 342 eventsIndex := NewEventsIndex(events) 343 344 switch tt.queryMode { 345 case IndexQueryModeLocalOnly, IndexQueryModeExecutionNodesOnly: 346 // not applicable 347 return 348 case IndexQueryModeFailover: 349 // only failing blocks queried from EN 350 s.setupENSuccessResponse(targetEvent, []*flow.Block{s.blocks[0], s.blocks[4]}) 351 } 352 353 // the first and last blocks are not available from storage, and should be fetched from the EN 354 reporter := syncmock.NewIndexReporter(s.T()) 355 reporter.On("LowestIndexedHeight").Return(s.blocks[1].Header.Height, nil) 356 reporter.On("HighestIndexedHeight").Return(s.blocks[3].Header.Height, nil) 357 358 events.On("ByBlockID", s.blockIDs[1]).Return(s.blockEvents, nil) 359 events.On("ByBlockID", s.blockIDs[2]).Return(s.blockEvents, nil) 360 events.On("ByBlockID", s.blockIDs[3]).Return(s.blockEvents, nil) 361 362 err := eventsIndex.Initialize(reporter) 363 s.Require().NoError(err) 364 365 backend := s.defaultBackend() 366 backend.queryMode = tt.queryMode 367 backend.eventsIndex = eventsIndex 368 369 response, err := backend.GetEventsForBlockIDs(ctx, targetEvent, s.blockIDs, tt.encoding) 370 s.Require().NoError(err) 371 s.assertResponse(response, tt.encoding) 372 373 response, err = backend.GetEventsForHeightRange(ctx, targetEvent, startHeight, endHeight, tt.encoding) 374 s.Require().NoError(err) 375 s.assertResponse(response, tt.encoding) 376 }) 377 } 378 } 379 380 func (s *BackendEventsSuite) TestGetEventsForHeightRange_HandlesErrors() { 381 ctx := context.Background() 382 383 startHeight := s.blocks[0].Header.Height 384 endHeight := s.sealedHead.Height 385 encoding := entities.EventEncodingVersion_CCF_V0 386 387 s.Run("returns error for endHeight < startHeight", func() { 388 backend := s.defaultBackend() 389 endHeight := startHeight - 1 390 391 response, err := backend.GetEventsForHeightRange(ctx, targetEvent, startHeight, endHeight, encoding) 392 s.Assert().Equal(codes.InvalidArgument, status.Code(err)) 393 s.Assert().Nil(response) 394 }) 395 396 s.Run("returns error for range larger than max", func() { 397 backend := s.defaultBackend() 398 endHeight := startHeight + DefaultMaxHeightRange 399 400 response, err := backend.GetEventsForHeightRange(ctx, targetEvent, startHeight, endHeight, encoding) 401 s.Assert().Equal(codes.InvalidArgument, status.Code(err)) 402 s.Assert().Nil(response) 403 }) 404 405 s.Run("throws irrecoverable if sealed header not available", func() { 406 s.state.On("Sealed").Return(s.snapshot) 407 s.snapshot.On("Head").Return(nil, storage.ErrNotFound).Once() 408 409 signCtxErr := irrecoverable.NewExceptionf("failed to lookup sealed header: %w", storage.ErrNotFound) 410 signalerCtx := irrecoverable.WithSignalerContext(context.Background(), 411 irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, signCtxErr)) 412 413 backend := s.defaultBackend() 414 415 response, err := backend.GetEventsForHeightRange(signalerCtx, targetEvent, startHeight, endHeight, encoding) 416 // these will never be returned in production 417 s.Assert().Equal(codes.Unknown, status.Code(err)) 418 s.Assert().Nil(response) 419 }) 420 421 s.state.On("Sealed").Return(s.snapshot) 422 s.snapshot.On("Head").Return(s.sealedHead, nil) 423 424 s.Run("returns error for startHeight > sealed height", func() { 425 backend := s.defaultBackend() 426 startHeight := s.sealedHead.Height + 1 427 endHeight := startHeight + 1 428 429 response, err := backend.GetEventsForHeightRange(ctx, targetEvent, startHeight, endHeight, encoding) 430 s.Assert().Equal(codes.OutOfRange, status.Code(err)) 431 s.Assert().Nil(response) 432 }) 433 } 434 435 func (s *BackendEventsSuite) TestGetEventsForBlockIDs_HandlesErrors() { 436 ctx := context.Background() 437 438 encoding := entities.EventEncodingVersion_CCF_V0 439 440 s.Run("returns error when too many blockIDs requested", func() { 441 backend := s.defaultBackend() 442 backend.maxHeightRange = 3 443 444 response, err := backend.GetEventsForBlockIDs(ctx, targetEvent, s.blockIDs, encoding) 445 s.Assert().Equal(codes.InvalidArgument, status.Code(err)) 446 s.Assert().Nil(response) 447 }) 448 449 s.Run("returns error for missing header", func() { 450 headers := storagemock.NewHeaders(s.T()) 451 backend := s.defaultBackend() 452 backend.headers = headers 453 454 for i, blockID := range s.blockIDs { 455 // return error on the last header 456 if i == len(s.blocks)-1 { 457 headers.On("ByBlockID", blockID).Return(nil, storage.ErrNotFound) 458 continue 459 } 460 461 headers.On("ByBlockID", blockID).Return(s.blocks[i].Header, nil) 462 } 463 464 response, err := backend.GetEventsForBlockIDs(ctx, targetEvent, s.blockIDs, encoding) 465 s.Assert().Equal(codes.NotFound, status.Code(err)) 466 s.Assert().Nil(response) 467 }) 468 } 469 470 func (s *BackendEventsSuite) assertResponse(response []flow.BlockEvents, encoding entities.EventEncodingVersion) { 471 s.Assert().Len(response, len(s.blocks)) 472 for i, block := range s.blocks { 473 s.Assert().Equal(block.Header.Height, response[i].BlockHeight) 474 s.Assert().Equal(block.Header.ID(), response[i].BlockID) 475 s.Assert().Len(response[i].Events, 1) 476 477 s.assertEncoding(&response[i].Events[0], encoding) 478 } 479 } 480 481 func (s *BackendEventsSuite) assertEncoding(event *flow.Event, encoding entities.EventEncodingVersion) { 482 var err error 483 switch encoding { 484 case entities.EventEncodingVersion_CCF_V0: 485 _, err = ccf.Decode(nil, event.Payload) 486 case entities.EventEncodingVersion_JSON_CDC_V0: 487 _, err = jsoncdc.Decode(nil, event.Payload) 488 default: 489 s.T().Errorf("unknown encoding: %s", encoding.String()) 490 } 491 s.Require().NoError(err) 492 }