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