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  }