github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/state_stream/backend/backend_account_statuses_test.go (about)

     1  package backend
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/mock"
    11  	"github.com/stretchr/testify/require"
    12  	"github.com/stretchr/testify/suite"
    13  	"google.golang.org/grpc/codes"
    14  	"google.golang.org/grpc/status"
    15  
    16  	"github.com/onflow/flow-go/engine/access/state_stream"
    17  	"github.com/onflow/flow-go/engine/access/subscription"
    18  	"github.com/onflow/flow-go/model/flow"
    19  	"github.com/onflow/flow-go/module/executiondatasync/execution_data"
    20  	"github.com/onflow/flow-go/utils/unittest"
    21  	"github.com/onflow/flow-go/utils/unittest/generator"
    22  )
    23  
    24  var testProtocolEventTypes = []flow.EventType{
    25  	state_stream.CoreEventAccountCreated,
    26  	state_stream.CoreEventAccountContractAdded,
    27  	state_stream.CoreEventAccountContractUpdated,
    28  }
    29  
    30  // Define the test type struct
    31  // The struct is used for testing different test cases of each endpoint from AccountStatusesBackend.
    32  type testType struct {
    33  	name            string // Test case name
    34  	highestBackfill int    // Highest backfill index
    35  	startValue      interface{}
    36  	filters         state_stream.AccountStatusFilter // Event filters
    37  }
    38  
    39  // BackendAccountStatusesSuite is a test suite for the AccountStatusesBackend functionality.
    40  // It is used to test the endpoints which enables users to subscribe to the streaming of account status changes.
    41  // It verified that each of endpoints works properly with expected data being returned. Also the suite tests
    42  // handling of expected errors in the SubscribeAccountStatuses.
    43  type BackendAccountStatusesSuite struct {
    44  	BackendExecutionDataSuite
    45  	accountCreatedAddress  flow.Address
    46  	accountContractAdded   flow.Address
    47  	accountContractUpdated flow.Address
    48  }
    49  
    50  func TestBackendAccountStatusesSuite(t *testing.T) {
    51  	suite.Run(t, new(BackendAccountStatusesSuite))
    52  }
    53  
    54  // generateProtocolMockEvents generates a set of mock events.
    55  func (s *BackendAccountStatusesSuite) generateProtocolMockEvents() flow.EventsList {
    56  	events := make([]flow.Event, 4)
    57  	events = append(events, unittest.EventFixture(testEventTypes[0], 0, 0, unittest.IdentifierFixture(), 0))
    58  
    59  	accountCreateEvent := generator.GenerateAccountCreateEvent(s.T(), s.accountCreatedAddress)
    60  	accountCreateEvent.TransactionIndex = 1
    61  	events = append(events, accountCreateEvent)
    62  
    63  	accountContractAdded := generator.GenerateAccountContractEvent(s.T(), "AccountContractAdded", s.accountContractAdded)
    64  	accountContractAdded.TransactionIndex = 2
    65  	events = append(events, accountContractAdded)
    66  
    67  	accountContractUpdated := generator.GenerateAccountContractEvent(s.T(), "AccountContractUpdated", s.accountContractUpdated)
    68  	accountContractUpdated.TransactionIndex = 3
    69  	events = append(events, accountContractUpdated)
    70  
    71  	return events
    72  }
    73  
    74  // SetupTest initializes the test suite.
    75  func (s *BackendAccountStatusesSuite) SetupTest() {
    76  	blockCount := 5
    77  	var err error
    78  	s.SetupTestSuite(blockCount)
    79  
    80  	addressGenerator := chainID.Chain().NewAddressGenerator()
    81  	s.accountCreatedAddress, err = addressGenerator.NextAddress()
    82  	require.NoError(s.T(), err)
    83  	s.accountContractAdded, err = addressGenerator.NextAddress()
    84  	require.NoError(s.T(), err)
    85  	s.accountContractUpdated, err = addressGenerator.NextAddress()
    86  	require.NoError(s.T(), err)
    87  
    88  	parent := s.rootBlock.Header
    89  	events := s.generateProtocolMockEvents()
    90  
    91  	for i := 0; i < blockCount; i++ {
    92  		block := unittest.BlockWithParentFixture(parent)
    93  		// update for next iteration
    94  		parent = block.Header
    95  
    96  		seal := unittest.BlockSealsFixture(1)[0]
    97  		result := unittest.ExecutionResultFixture()
    98  
    99  		chunkDatas := []*execution_data.ChunkExecutionData{
   100  			unittest.ChunkExecutionDataFixture(s.T(), execution_data.DefaultMaxBlobSize/5, unittest.WithChunkEvents(events)),
   101  		}
   102  
   103  		execData := unittest.BlockExecutionDataFixture(
   104  			unittest.WithBlockExecutionDataBlockID(block.ID()),
   105  			unittest.WithChunkExecutionDatas(chunkDatas...),
   106  		)
   107  
   108  		result.ExecutionDataID, err = s.eds.Add(context.TODO(), execData)
   109  		assert.NoError(s.T(), err)
   110  
   111  		s.blocks = append(s.blocks, block)
   112  		s.execDataMap[block.ID()] = execution_data.NewBlockExecutionDataEntity(result.ExecutionDataID, execData)
   113  		s.blockEvents[block.ID()] = events
   114  		s.blockMap[block.Header.Height] = block
   115  		s.sealMap[block.ID()] = seal
   116  		s.resultMap[seal.ResultID] = result
   117  
   118  		s.T().Logf("adding exec data for block %d %d %v => %v", i, block.Header.Height, block.ID(), result.ExecutionDataID)
   119  	}
   120  
   121  	s.SetupTestMocks()
   122  }
   123  
   124  // subscribeFromStartBlockIdTestCases generates test cases for subscribing from a start block ID.
   125  func (s *BackendAccountStatusesSuite) subscribeFromStartBlockIdTestCases() []testType {
   126  	baseTests := []testType{
   127  		{
   128  			name:            "happy path - all new blocks",
   129  			highestBackfill: -1, // no backfill
   130  			startValue:      s.rootBlock.ID(),
   131  		},
   132  		{
   133  			name:            "happy path - partial backfill",
   134  			highestBackfill: 2, // backfill the first 3 blocks
   135  			startValue:      s.blocks[0].ID(),
   136  		},
   137  		{
   138  			name:            "happy path - complete backfill",
   139  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   140  			startValue:      s.blocks[0].ID(),
   141  		},
   142  		{
   143  			name:            "happy path - start from root block by id",
   144  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   145  			startValue:      s.rootBlock.ID(),  // start from root block
   146  		},
   147  	}
   148  
   149  	return s.generateFiltersForTestCases(baseTests)
   150  }
   151  
   152  // subscribeFromStartHeightTestCases generates test cases for subscribing from a start height.
   153  func (s *BackendAccountStatusesSuite) subscribeFromStartHeightTestCases() []testType {
   154  	baseTests := []testType{
   155  		{
   156  			name:            "happy path - all new blocks",
   157  			highestBackfill: -1, // no backfill
   158  			startValue:      s.rootBlock.Header.Height,
   159  		},
   160  		{
   161  			name:            "happy path - partial backfill",
   162  			highestBackfill: 2, // backfill the first 3 blocks
   163  			startValue:      s.blocks[0].Header.Height,
   164  		},
   165  		{
   166  			name:            "happy path - complete backfill",
   167  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   168  			startValue:      s.blocks[0].Header.Height,
   169  		},
   170  		{
   171  			name:            "happy path - start from root block by id",
   172  			highestBackfill: len(s.blocks) - 1,         // backfill all blocks
   173  			startValue:      s.rootBlock.Header.Height, // start from root block
   174  		},
   175  	}
   176  
   177  	return s.generateFiltersForTestCases(baseTests)
   178  }
   179  
   180  // subscribeFromLatestTestCases generates test cases for subscribing from the latest block.
   181  func (s *BackendAccountStatusesSuite) subscribeFromLatestTestCases() []testType {
   182  	baseTests := []testType{
   183  		{
   184  			name:            "happy path - all new blocks",
   185  			highestBackfill: -1, // no backfill
   186  		},
   187  		{
   188  			name:            "happy path - partial backfill",
   189  			highestBackfill: 2, // backfill the first 3 blocks
   190  		},
   191  		{
   192  			name:            "happy path - complete backfill",
   193  			highestBackfill: len(s.blocks) - 1, // backfill all blocks
   194  		},
   195  	}
   196  
   197  	return s.generateFiltersForTestCases(baseTests)
   198  }
   199  
   200  // generateFiltersForTestCases generates variations of test cases with different event filters.
   201  //
   202  // This function takes an array of base testType structs and creates variations for each of them.
   203  // For each base test case, it generates three variations:
   204  // - All events: Includes all protocol event types filtered by the provided account address.
   205  // - Some events: Includes only the first protocol event type filtered by the provided account address.
   206  // - No events: Includes a custom event type "flow.AccountKeyAdded" filtered by the provided account address.
   207  func (s *BackendAccountStatusesSuite) generateFiltersForTestCases(baseTests []testType) []testType {
   208  	// Create variations for each of the base tests
   209  	tests := make([]testType, 0, len(baseTests)*3)
   210  	var err error
   211  	for _, test := range baseTests {
   212  		t1 := test
   213  		t1.name = fmt.Sprintf("%s - all events", test.name)
   214  		t1.filters, err = state_stream.NewAccountStatusFilter(
   215  			state_stream.DefaultEventFilterConfig,
   216  			chainID.Chain(),
   217  			[]string{string(testProtocolEventTypes[0]), string(testProtocolEventTypes[1]), string(testProtocolEventTypes[2])},
   218  			[]string{s.accountCreatedAddress.HexWithPrefix(), s.accountContractAdded.HexWithPrefix(), s.accountContractUpdated.HexWithPrefix()},
   219  		)
   220  		require.NoError(s.T(), err)
   221  		tests = append(tests, t1)
   222  
   223  		t2 := test
   224  		t2.name = fmt.Sprintf("%s - some events", test.name)
   225  		t2.filters, err = state_stream.NewAccountStatusFilter(
   226  			state_stream.DefaultEventFilterConfig,
   227  			chainID.Chain(),
   228  			[]string{string(testProtocolEventTypes[0])},
   229  			[]string{s.accountCreatedAddress.HexWithPrefix(), s.accountContractAdded.HexWithPrefix(), s.accountContractUpdated.HexWithPrefix()},
   230  		)
   231  		require.NoError(s.T(), err)
   232  		tests = append(tests, t2)
   233  
   234  		t3 := test
   235  		t3.name = fmt.Sprintf("%s - no events", test.name)
   236  		t3.filters, err = state_stream.NewAccountStatusFilter(
   237  			state_stream.DefaultEventFilterConfig,
   238  			chainID.Chain(),
   239  			[]string{"flow.AccountKeyAdded"},
   240  			[]string{s.accountCreatedAddress.HexWithPrefix(), s.accountContractAdded.HexWithPrefix(), s.accountContractUpdated.HexWithPrefix()},
   241  		)
   242  		require.NoError(s.T(), err)
   243  		tests = append(tests, t3)
   244  
   245  		t4 := test
   246  		t4.name = fmt.Sprintf("%s - no events, no addresses", test.name)
   247  		t4.filters, err = state_stream.NewAccountStatusFilter(
   248  			state_stream.DefaultEventFilterConfig,
   249  			chainID.Chain(),
   250  			[]string{},
   251  			[]string{},
   252  		)
   253  		require.NoError(s.T(), err)
   254  		tests = append(tests, t4)
   255  
   256  		t5 := test
   257  		t5.name = fmt.Sprintf("%s - some events, no addresses", test.name)
   258  		t5.filters, err = state_stream.NewAccountStatusFilter(
   259  			state_stream.DefaultEventFilterConfig,
   260  			chainID.Chain(),
   261  			[]string{"flow.AccountKeyAdded"},
   262  			[]string{},
   263  		)
   264  		require.NoError(s.T(), err)
   265  		tests = append(tests, t5)
   266  	}
   267  
   268  	return tests
   269  }
   270  
   271  // subscribeToAccountStatuses runs subscription tests for account statuses.
   272  //
   273  // This function takes a subscribeFn function, which is a subscription function for account statuses,
   274  // and an array of testType structs representing the test cases.
   275  // It iterates over each test case and sets up the necessary context and cancellation for the subscription.
   276  // For each test case, it simulates backfill blocks and verifies the expected account events for each block.
   277  // It also ensures that the subscription shuts down gracefully after completing the test cases.
   278  func (s *BackendAccountStatusesSuite) subscribeToAccountStatuses(
   279  	subscribeFn func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription,
   280  	tests []testType,
   281  ) {
   282  	ctx, cancel := context.WithCancel(context.Background())
   283  	defer cancel()
   284  
   285  	// Iterate over each test case
   286  	for _, test := range tests {
   287  		s.Run(test.name, func() {
   288  			s.T().Logf("len(s.execDataMap) %d", len(s.execDataMap))
   289  
   290  			// Add "backfill" block - blocks that are already in the database before the test starts
   291  			// This simulates a subscription on a past block
   292  			if test.highestBackfill > 0 {
   293  				s.highestBlockHeader = s.blocks[test.highestBackfill].Header
   294  			}
   295  
   296  			// Set up subscription context and cancellation
   297  			subCtx, subCancel := context.WithCancel(ctx)
   298  
   299  			sub := subscribeFn(subCtx, test.startValue, test.filters)
   300  
   301  			// Loop over all the blocks
   302  			for i, b := range s.blocks {
   303  				s.T().Logf("checking block %d %v", i, b.ID())
   304  
   305  				// Simulate new exec data received.
   306  				// Exec data for all blocks with index <= highestBackfill were already received
   307  				if i > test.highestBackfill {
   308  					s.highestBlockHeader = b.Header
   309  
   310  					s.broadcaster.Publish()
   311  				}
   312  
   313  				expectedEvents := map[string]flow.EventsList{}
   314  				for _, event := range s.blockEvents[b.ID()] {
   315  					if test.filters.Match(event) {
   316  						var address string
   317  						switch event.Type {
   318  						case state_stream.CoreEventAccountCreated:
   319  							address = s.accountCreatedAddress.HexWithPrefix()
   320  						case state_stream.CoreEventAccountContractAdded:
   321  							address = s.accountContractAdded.HexWithPrefix()
   322  						case state_stream.CoreEventAccountContractUpdated:
   323  							address = s.accountContractUpdated.HexWithPrefix()
   324  						}
   325  						expectedEvents[address] = append(expectedEvents[address], event)
   326  					}
   327  				}
   328  
   329  				// Consume execution data from subscription
   330  				unittest.RequireReturnsBefore(s.T(), func() {
   331  					v, ok := <-sub.Channel()
   332  					require.True(s.T(), ok, "channel closed while waiting for exec data for block %d %v: err: %v", b.Header.Height, b.ID(), sub.Err())
   333  
   334  					resp, ok := v.(*AccountStatusesResponse)
   335  					require.True(s.T(), ok, "unexpected response type: %T", v)
   336  
   337  					assert.Equal(s.T(), b.Header.ID(), resp.BlockID)
   338  					assert.Equal(s.T(), b.Header.Height, resp.Height)
   339  					assert.Equal(s.T(), expectedEvents, resp.AccountEvents)
   340  				}, 60*time.Second, fmt.Sprintf("timed out waiting for exec data for block %d %v", b.Header.Height, b.ID()))
   341  			}
   342  
   343  			// Make sure there are no new messages waiting. The channel should be opened with nothing waiting
   344  			unittest.RequireNeverReturnBefore(s.T(), func() {
   345  				<-sub.Channel()
   346  			}, 100*time.Millisecond, "timed out waiting for subscription to shutdown")
   347  
   348  			// Stop the subscription
   349  			subCancel()
   350  
   351  			// Ensure subscription shuts down gracefully
   352  			unittest.RequireReturnsBefore(s.T(), func() {
   353  				v, ok := <-sub.Channel()
   354  				assert.Nil(s.T(), v)
   355  				assert.False(s.T(), ok)
   356  				assert.ErrorIs(s.T(), sub.Err(), context.Canceled)
   357  			}, 100*time.Millisecond, "timed out waiting for subscription to shutdown")
   358  		})
   359  	}
   360  }
   361  
   362  // TestSubscribeAccountStatusesFromStartBlockID tests the SubscribeAccountStatusesFromStartBlockID method.
   363  func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromStartBlockID() {
   364  	s.executionDataTracker.On(
   365  		"GetStartHeightFromBlockID",
   366  		mock.AnythingOfType("flow.Identifier"),
   367  	).Return(func(startBlockID flow.Identifier) (uint64, error) {
   368  		return s.executionDataTrackerReal.GetStartHeightFromBlockID(startBlockID)
   369  	}, nil)
   370  
   371  	call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription {
   372  		return s.backend.SubscribeAccountStatusesFromStartBlockID(ctx, startValue.(flow.Identifier), filter)
   373  	}
   374  
   375  	s.subscribeToAccountStatuses(call, s.subscribeFromStartBlockIdTestCases())
   376  }
   377  
   378  // TestSubscribeAccountStatusesFromStartHeight tests the SubscribeAccountStatusesFromStartHeight method.
   379  func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromStartHeight() {
   380  	s.executionDataTracker.On(
   381  		"GetStartHeightFromHeight",
   382  		mock.AnythingOfType("uint64"),
   383  	).Return(func(startHeight uint64) (uint64, error) {
   384  		return s.executionDataTrackerReal.GetStartHeightFromHeight(startHeight)
   385  	}, nil)
   386  
   387  	call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription {
   388  		return s.backend.SubscribeAccountStatusesFromStartHeight(ctx, startValue.(uint64), filter)
   389  	}
   390  
   391  	s.subscribeToAccountStatuses(call, s.subscribeFromStartHeightTestCases())
   392  }
   393  
   394  // TestSubscribeAccountStatusesFromLatestBlock tests the SubscribeAccountStatusesFromLatestBlock method.
   395  func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromLatestBlock() {
   396  	s.executionDataTracker.On(
   397  		"GetStartHeightFromLatest",
   398  		mock.Anything,
   399  	).Return(func(ctx context.Context) (uint64, error) {
   400  		return s.executionDataTrackerReal.GetStartHeightFromLatest(ctx)
   401  	}, nil)
   402  
   403  	call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription {
   404  		return s.backend.SubscribeAccountStatusesFromLatestBlock(ctx, filter)
   405  	}
   406  
   407  	s.subscribeToAccountStatuses(call, s.subscribeFromLatestTestCases())
   408  }
   409  
   410  // TestSubscribeAccountStatusesHandlesErrors tests handling of expected errors in the SubscribeAccountStatuses.
   411  func (s *BackendExecutionDataSuite) TestSubscribeAccountStatusesHandlesErrors() {
   412  	ctx, cancel := context.WithCancel(context.Background())
   413  	defer cancel()
   414  
   415  	// mock block tracker for SubscribeBlocksFromStartBlockID
   416  	s.executionDataTracker.On(
   417  		"GetStartHeightFromBlockID",
   418  		mock.AnythingOfType("flow.Identifier"),
   419  	).Return(func(startBlockID flow.Identifier) (uint64, error) {
   420  		return s.executionDataTrackerReal.GetStartHeightFromBlockID(startBlockID)
   421  	}, nil)
   422  
   423  	s.Run("returns error for unindexed start blockID", func() {
   424  		subCtx, subCancel := context.WithCancel(ctx)
   425  		defer subCancel()
   426  
   427  		sub := s.backend.SubscribeAccountStatusesFromStartBlockID(subCtx, unittest.IdentifierFixture(), state_stream.AccountStatusFilter{})
   428  		assert.Equal(s.T(), codes.NotFound, status.Code(sub.Err()), "expected NotFound, got %v: %v", status.Code(sub.Err()).String(), sub.Err())
   429  	})
   430  
   431  	s.executionDataTracker.On(
   432  		"GetStartHeightFromHeight",
   433  		mock.AnythingOfType("uint64"),
   434  	).Return(func(startHeight uint64) (uint64, error) {
   435  		return s.executionDataTrackerReal.GetStartHeightFromHeight(startHeight)
   436  	}, nil)
   437  
   438  	s.Run("returns error for start height before root height", func() {
   439  		subCtx, subCancel := context.WithCancel(ctx)
   440  		defer subCancel()
   441  
   442  		sub := s.backend.SubscribeAccountStatusesFromStartHeight(subCtx, s.rootBlock.Header.Height-1, state_stream.AccountStatusFilter{})
   443  		assert.Equal(s.T(), codes.InvalidArgument, status.Code(sub.Err()), "expected InvalidArgument, got %v: %v", status.Code(sub.Err()).String(), sub.Err())
   444  	})
   445  
   446  	// make sure we're starting with a fresh cache
   447  	s.execDataHeroCache.Clear()
   448  
   449  	s.Run("returns error for unindexed start height", func() {
   450  		subCtx, subCancel := context.WithCancel(ctx)
   451  		defer subCancel()
   452  
   453  		sub := s.backend.SubscribeAccountStatusesFromStartHeight(subCtx, s.blocks[len(s.blocks)-1].Header.Height+10, state_stream.AccountStatusFilter{})
   454  		assert.Equal(s.T(), codes.NotFound, status.Code(sub.Err()), "expected NotFound, got %v: %v", status.Code(sub.Err()).String(), sub.Err())
   455  	})
   456  }