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  }