github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/common/synchronization/engine_test.go (about)

     1  package synchronization
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/mock"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/onflow/flow-go/model/flow"
    14  	"github.com/onflow/flow-go/model/flow/filter"
    15  	"github.com/onflow/flow-go/model/messages"
    16  	synccore "github.com/onflow/flow-go/module/chainsync"
    17  	"github.com/onflow/flow-go/module/irrecoverable"
    18  	"github.com/onflow/flow-go/module/metrics"
    19  	netint "github.com/onflow/flow-go/network"
    20  	"github.com/onflow/flow-go/network/channels"
    21  	"github.com/onflow/flow-go/utils/rand"
    22  	"github.com/onflow/flow-go/utils/unittest"
    23  )
    24  
    25  // TestOnSyncRequest_LowerThanReceiver_WithinTolerance tests that a sync request that's within tolerance of the receiver doesn't trigger
    26  // a response, even if request height is lower than receiver.
    27  func (ss *SyncSuite) TestOnSyncRequest_LowerThanReceiver_WithinTolerance() {
    28  	nonce, err := rand.Uint64()
    29  	require.NoError(ss.T(), err, "should generate nonce")
    30  	// generate origin and request message
    31  	originID := unittest.IdentifierFixture()
    32  	req := &messages.SyncRequest{
    33  		Nonce:  nonce,
    34  		Height: 0,
    35  	}
    36  
    37  	// regardless of request height, if within tolerance, we should not respond
    38  	ss.core.On("HandleHeight", ss.head, req.Height)
    39  	ss.core.On("WithinTolerance", ss.head, req.Height).Return(true)
    40  	ss.Assert().NoError(ss.e.requestHandler.onSyncRequest(originID, req))
    41  	ss.con.AssertNotCalled(ss.T(), "Unicast", mock.Anything, mock.Anything)
    42  	ss.core.AssertExpectations(ss.T())
    43  }
    44  
    45  // TestOnSyncRequest_HigherThanReceiver_OutsideTolerance tests that a sync request that's higher
    46  // than the receiver's height doesn't trigger a response, even if outside tolerance.
    47  func (ss *SyncSuite) TestOnSyncRequest_HigherThanReceiver_OutsideTolerance() {
    48  	nonce, err := rand.Uint64()
    49  	require.NoError(ss.T(), err, "should generate nonce")
    50  	// generate origin and request message
    51  	originID := unittest.IdentifierFixture()
    52  	req := &messages.SyncRequest{
    53  		Nonce:  nonce,
    54  		Height: 0,
    55  	}
    56  
    57  	// if request height is higher than local finalized, we should not respond
    58  	req.Height = ss.head.Height + 1
    59  
    60  	ss.core.On("HandleHeight", ss.head, req.Height)
    61  	ss.core.On("WithinTolerance", ss.head, req.Height).Return(false)
    62  	ss.Assert().NoError(ss.e.requestHandler.onSyncRequest(originID, req))
    63  	ss.con.AssertNotCalled(ss.T(), "Unicast", mock.Anything, mock.Anything)
    64  	ss.core.AssertExpectations(ss.T())
    65  }
    66  
    67  // TestOnSyncRequest_LowerThanReceiver_OutsideTolerance tests that a sync request that's outside tolerance and
    68  // lower than the receiver's height triggers a response.
    69  func (ss *SyncSuite) TestOnSyncRequest_LowerThanReceiver_OutsideTolerance() {
    70  	nonce, err := rand.Uint64()
    71  	require.NoError(ss.T(), err, "should generate nonce")
    72  
    73  	// generate origin and request message
    74  	originID := unittest.IdentifierFixture()
    75  	req := &messages.SyncRequest{
    76  		Nonce:  nonce,
    77  		Height: 0,
    78  	}
    79  
    80  	// if the request height is lower than head and outside tolerance, we should expect correct response
    81  	req.Height = ss.head.Height - 1
    82  	ss.core.On("HandleHeight", ss.head, req.Height)
    83  	ss.core.On("WithinTolerance", ss.head, req.Height).Return(false)
    84  	ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Run(
    85  		func(args mock.Arguments) {
    86  			res := args.Get(0).(*messages.SyncResponse)
    87  			assert.Equal(ss.T(), ss.head.Height, res.Height, "response should contain head height")
    88  			assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
    89  			recipientID := args.Get(1).(flow.Identifier)
    90  			assert.Equal(ss.T(), originID, recipientID, "should send response to original sender")
    91  		},
    92  	)
    93  	err = ss.e.requestHandler.onSyncRequest(originID, req)
    94  	require.NoError(ss.T(), err, "smaller height sync request should pass")
    95  
    96  	ss.core.AssertExpectations(ss.T())
    97  }
    98  
    99  func (ss *SyncSuite) TestOnSyncResponse() {
   100  	nonce, err := rand.Uint64()
   101  	require.NoError(ss.T(), err, "should generate nonce")
   102  
   103  	height, err := rand.Uint64()
   104  	require.NoError(ss.T(), err, "should generate height")
   105  
   106  	// generate origin ID and response message
   107  	originID := unittest.IdentifierFixture()
   108  	res := &messages.SyncResponse{
   109  		Nonce:  nonce,
   110  		Height: height,
   111  	}
   112  
   113  	// the height should be handled
   114  	ss.core.On("HandleHeight", ss.head, res.Height)
   115  	ss.e.onSyncResponse(originID, res)
   116  	ss.core.AssertExpectations(ss.T())
   117  }
   118  
   119  func (ss *SyncSuite) TestOnRangeRequest() {
   120  	nonce, err := rand.Uint64()
   121  	require.NoError(ss.T(), err, "should generate nonce")
   122  
   123  	// generate originID and range request
   124  	originID := unittest.IdentifierFixture()
   125  	req := &messages.RangeRequest{
   126  		Nonce:      nonce,
   127  		FromHeight: 0,
   128  		ToHeight:   0,
   129  	}
   130  
   131  	// fill in blocks at heights -1 to -4 from head
   132  	ref := ss.head.Height
   133  	for height := ref; height >= ref-4; height-- {
   134  		block := unittest.BlockFixture()
   135  		block.Header.Height = height
   136  		ss.heights[height] = &block
   137  	}
   138  
   139  	// empty range should be a no-op
   140  	ss.T().Run("empty range", func(t *testing.T) {
   141  		req.FromHeight = ref
   142  		req.ToHeight = ref - 1
   143  		err := ss.e.requestHandler.onRangeRequest(originID, req)
   144  		require.NoError(ss.T(), err, "empty range request should pass")
   145  		ss.con.AssertNotCalled(ss.T(), "Unicast", mock.Anything, mock.Anything)
   146  	})
   147  
   148  	// range with only unknown block should be a no-op
   149  	ss.T().Run("range with unknown block", func(t *testing.T) {
   150  		req.FromHeight = ref + 1
   151  		req.ToHeight = ref + 3
   152  		err := ss.e.requestHandler.onRangeRequest(originID, req)
   153  		require.NoError(ss.T(), err, "unknown range request should pass")
   154  		ss.con.AssertNotCalled(ss.T(), "Unicast", mock.Anything, mock.Anything)
   155  	})
   156  
   157  	// a request for same from and to should send single block
   158  	ss.T().Run("from == to", func(t *testing.T) {
   159  		req.FromHeight = ref - 1
   160  		req.ToHeight = ref - 1
   161  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Once().Run(
   162  			func(args mock.Arguments) {
   163  				res := args.Get(0).(*messages.BlockResponse)
   164  				expected := ss.heights[ref-1]
   165  				actual := res.Blocks[0].ToInternal()
   166  				assert.Equal(ss.T(), expected, actual, "response should contain right block")
   167  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   168  				recipientID := args.Get(1).(flow.Identifier)
   169  				assert.Equal(ss.T(), originID, recipientID, "should send response to original requester")
   170  			},
   171  		)
   172  		err := ss.e.requestHandler.onRangeRequest(originID, req)
   173  		require.NoError(ss.T(), err, "range request with higher to height should pass")
   174  		ss.con.AssertNumberOfCalls(ss.T(), "Unicast", 1)
   175  
   176  		// clear any expectations for next test - otherwise, next subtest will fail due to increment of expected calls to Unicast
   177  		ss.con.Mock = mock.Mock{}
   178  	})
   179  
   180  	// a request for a range that we partially have should send partial response
   181  	ss.T().Run("have partial range", func(t *testing.T) {
   182  		req.FromHeight = ref - 2
   183  		req.ToHeight = ref + 2
   184  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Once().Run(
   185  			func(args mock.Arguments) {
   186  				res := args.Get(0).(*messages.BlockResponse)
   187  				expected := []*flow.Block{ss.heights[ref-2], ss.heights[ref-1], ss.heights[ref]}
   188  				assert.ElementsMatch(ss.T(), expected, res.BlocksInternal(), "response should contain right blocks")
   189  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   190  				recipientID := args.Get(1).(flow.Identifier)
   191  				assert.Equal(ss.T(), originID, recipientID, "should send response to original requester")
   192  			},
   193  		)
   194  		err := ss.e.requestHandler.onRangeRequest(originID, req)
   195  		require.NoError(ss.T(), err, "valid range with missing blocks should fail")
   196  		ss.con.AssertNumberOfCalls(ss.T(), "Unicast", 1)
   197  
   198  		// clear any expectations for next test - otherwise, next subtest will fail due to increment of expected calls to Unicast
   199  		ss.con.Mock = mock.Mock{}
   200  	})
   201  
   202  	// a request for a range we entirely have should send all blocks
   203  	ss.T().Run("have entire range", func(t *testing.T) {
   204  		req.FromHeight = ref - 2
   205  		req.ToHeight = ref
   206  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Once().Run(
   207  			func(args mock.Arguments) {
   208  				res := args.Get(0).(*messages.BlockResponse)
   209  				expected := []*flow.Block{ss.heights[ref-2], ss.heights[ref-1], ss.heights[ref]}
   210  				assert.ElementsMatch(ss.T(), expected, res.BlocksInternal(), "response should contain right blocks")
   211  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   212  				recipientID := args.Get(1).(flow.Identifier)
   213  				assert.Equal(ss.T(), originID, recipientID, "should send response to original requester")
   214  			},
   215  		)
   216  		err := ss.e.requestHandler.onRangeRequest(originID, req)
   217  		require.NoError(ss.T(), err, "valid range request should pass")
   218  		ss.con.AssertNumberOfCalls(ss.T(), "Unicast", 1)
   219  
   220  		// clear any expectations for next test - otherwise, next subtest will fail due to increment of expected calls to Unicast
   221  		ss.con.Mock = mock.Mock{}
   222  	})
   223  
   224  	// a request for a range larger than MaxSize should be clamped
   225  	ss.T().Run("oversized range", func(t *testing.T) {
   226  		req.FromHeight = ref - 4
   227  		req.ToHeight = math.MaxUint64
   228  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Once().Run(
   229  			func(args mock.Arguments) {
   230  				res := args.Get(0).(*messages.BlockResponse)
   231  				expected := []*flow.Block{ss.heights[ref-4], ss.heights[ref-3], ss.heights[ref-2]}
   232  				assert.ElementsMatch(ss.T(), expected, res.BlocksInternal(), "response should contain right blocks")
   233  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   234  				recipientID := args.Get(1).(flow.Identifier)
   235  				assert.Equal(ss.T(), originID, recipientID, "should send response to original requester")
   236  			},
   237  		)
   238  
   239  		// Rebuild sync core with a smaller max size
   240  		var err error
   241  		config := synccore.DefaultConfig()
   242  		config.MaxSize = 2
   243  		ss.e.requestHandler.core, err = synccore.New(ss.e.log, config, metrics.NewNoopCollector(), flow.Localnet)
   244  		require.NoError(ss.T(), err)
   245  
   246  		err = ss.e.requestHandler.onRangeRequest(originID, req)
   247  		require.NoError(ss.T(), err, "valid range request exceeding max size should still pass")
   248  		ss.con.AssertNumberOfCalls(ss.T(), "Unicast", 1)
   249  
   250  		// clear any expectations for next test - otherwise, next subtest will fail due to increment of expected calls to Unicast
   251  		ss.con.Mock = mock.Mock{}
   252  	})
   253  }
   254  
   255  func (ss *SyncSuite) TestOnBatchRequest() {
   256  	nonce, err := rand.Uint64()
   257  	require.NoError(ss.T(), err, "should generate nonce")
   258  
   259  	// generate origin ID and batch request
   260  	originID := unittest.IdentifierFixture()
   261  	req := &messages.BatchRequest{
   262  		Nonce:    nonce,
   263  		BlockIDs: nil,
   264  	}
   265  
   266  	// an empty request should not lead to response
   267  	ss.T().Run("empty request", func(t *testing.T) {
   268  		req.BlockIDs = []flow.Identifier{}
   269  		err := ss.e.requestHandler.onBatchRequest(originID, req)
   270  		require.NoError(ss.T(), err, "should pass empty request")
   271  		ss.con.AssertNumberOfCalls(ss.T(), "Unicast", 0)
   272  	})
   273  
   274  	// a non-empty request for missing block ID should be a no-op
   275  	ss.T().Run("request for missing blocks", func(t *testing.T) {
   276  		req.BlockIDs = unittest.IdentifierListFixture(1)
   277  		err := ss.e.requestHandler.onBatchRequest(originID, req)
   278  		require.NoError(ss.T(), err, "should pass request for missing block")
   279  		ss.con.AssertNumberOfCalls(ss.T(), "Unicast", 0)
   280  	})
   281  
   282  	// a non-empty request for existing block IDs should send right response
   283  	ss.T().Run("request for existing blocks", func(t *testing.T) {
   284  		block := unittest.BlockFixture()
   285  		block.Header.Height = ss.head.Height - 1
   286  		req.BlockIDs = []flow.Identifier{block.ID()}
   287  		ss.blockIDs[block.ID()] = &block
   288  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Run(
   289  			func(args mock.Arguments) {
   290  				res := args.Get(0).(*messages.BlockResponse)
   291  				assert.Equal(ss.T(), &block, res.Blocks[0].ToInternal(), "response should contain right block")
   292  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   293  				recipientID := args.Get(1).(flow.Identifier)
   294  				assert.Equal(ss.T(), originID, recipientID, "response should be send to original requester")
   295  			},
   296  		).Once()
   297  		err := ss.e.requestHandler.onBatchRequest(originID, req)
   298  		require.NoError(ss.T(), err, "should pass request with valid block")
   299  	})
   300  
   301  	// a request for too many blocks should be clamped
   302  	ss.T().Run("oversized range", func(t *testing.T) {
   303  		// setup request for 5 blocks. response should contain the first 2 (MaxSize)
   304  		ss.blockIDs = make(map[flow.Identifier]*flow.Block)
   305  		req.BlockIDs = make([]flow.Identifier, 5)
   306  		for i := 0; i < len(req.BlockIDs); i++ {
   307  			b := unittest.BlockFixture()
   308  			b.Header.Height = ss.head.Height - uint64(i)
   309  			req.BlockIDs[i] = b.ID()
   310  			ss.blockIDs[b.ID()] = &b
   311  		}
   312  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Run(
   313  			func(args mock.Arguments) {
   314  				res := args.Get(0).(*messages.BlockResponse)
   315  				assert.ElementsMatch(ss.T(), []*flow.Block{ss.blockIDs[req.BlockIDs[0]], ss.blockIDs[req.BlockIDs[1]]}, res.BlocksInternal(), "response should contain right block")
   316  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   317  				recipientID := args.Get(1).(flow.Identifier)
   318  				assert.Equal(ss.T(), originID, recipientID, "response should be send to original requester")
   319  			},
   320  		)
   321  
   322  		// Rebuild sync core with a smaller max size
   323  		var err error
   324  		config := synccore.DefaultConfig()
   325  		config.MaxSize = 2
   326  		ss.e.requestHandler.core, err = synccore.New(ss.e.log, config, metrics.NewNoopCollector(), flow.Localnet)
   327  		require.NoError(ss.T(), err)
   328  
   329  		err = ss.e.requestHandler.onBatchRequest(originID, req)
   330  		require.NoError(ss.T(), err, "valid batch request exceeding max size should still pass")
   331  	})
   332  }
   333  
   334  func (ss *SyncSuite) TestOnBlockResponse() {
   335  	nonce, err := rand.Uint64()
   336  	require.NoError(ss.T(), err, "should generate nonce")
   337  
   338  	// generate origin and block response
   339  	originID := unittest.IdentifierFixture()
   340  	res := &messages.BlockResponse{
   341  		Nonce:  nonce,
   342  		Blocks: []messages.UntrustedBlock{},
   343  	}
   344  
   345  	// add one block that should be processed
   346  	processable := unittest.BlockFixture()
   347  	ss.core.On("HandleBlock", processable.Header).Return(true)
   348  	res.Blocks = append(res.Blocks, messages.UntrustedBlockFromInternal(&processable))
   349  
   350  	// add one block that should not be processed
   351  	unprocessable := unittest.BlockFixture()
   352  	ss.core.On("HandleBlock", unprocessable.Header).Return(false)
   353  	res.Blocks = append(res.Blocks, messages.UntrustedBlockFromInternal(&unprocessable))
   354  
   355  	ss.comp.On("OnSyncedBlocks", mock.Anything).Run(func(args mock.Arguments) {
   356  		res := args.Get(0).(flow.Slashable[[]*messages.BlockProposal])
   357  		converted := res.Message[0].Block.ToInternal()
   358  		ss.Assert().Equal(processable.Header, converted.Header)
   359  		ss.Assert().Equal(processable.Payload, converted.Payload)
   360  		ss.Assert().Equal(originID, res.OriginID)
   361  	})
   362  
   363  	ss.e.onBlockResponse(originID, res)
   364  	ss.core.AssertExpectations(ss.T())
   365  }
   366  
   367  func (ss *SyncSuite) TestPollHeight() {
   368  
   369  	// check that we send to three nodes from our total list
   370  	others := ss.participants.Filter(filter.HasNodeID[flow.Identity](ss.participants[1:].NodeIDs()...))
   371  	ss.con.On("Multicast", mock.Anything, synccore.DefaultPollNodes, others[0].NodeID, others[1].NodeID).Return(nil).Run(
   372  		func(args mock.Arguments) {
   373  			req := args.Get(0).(*messages.SyncRequest)
   374  			require.Equal(ss.T(), ss.head.Height, req.Height, "request should contain finalized height")
   375  		},
   376  	)
   377  	ss.e.pollHeight()
   378  	ss.con.AssertExpectations(ss.T())
   379  }
   380  
   381  func (ss *SyncSuite) TestSendRequests() {
   382  
   383  	ranges := unittest.RangeListFixture(1)
   384  	batches := unittest.BatchListFixture(1)
   385  
   386  	// should submit and mark requested all ranges
   387  	ss.con.On("Multicast", mock.AnythingOfType("*messages.RangeRequest"), synccore.DefaultBlockRequestNodes, mock.Anything, mock.Anything).Return(nil).Run(
   388  		func(args mock.Arguments) {
   389  			req := args.Get(0).(*messages.RangeRequest)
   390  			ss.Assert().Equal(ranges[0].From, req.FromHeight)
   391  			ss.Assert().Equal(ranges[0].To, req.ToHeight)
   392  		},
   393  	)
   394  	ss.core.On("RangeRequested", ranges[0])
   395  
   396  	// should submit and mark requested all batches
   397  	ss.con.On("Multicast", mock.AnythingOfType("*messages.BatchRequest"), synccore.DefaultBlockRequestNodes, mock.Anything, mock.Anything, mock.Anything).Return(nil).Run(
   398  		func(args mock.Arguments) {
   399  			req := args.Get(0).(*messages.BatchRequest)
   400  			ss.Assert().Equal(batches[0].BlockIDs, req.BlockIDs)
   401  		},
   402  	)
   403  	ss.core.On("BatchRequested", batches[0])
   404  
   405  	// exclude my node ID
   406  	ss.e.sendRequests(ss.participants[1:].NodeIDs(), ranges, batches)
   407  	ss.con.AssertExpectations(ss.T())
   408  }
   409  
   410  // test a synchronization engine can be started and stopped
   411  func (ss *SyncSuite) TestStartStop() {
   412  	ctx, cancel := irrecoverable.NewMockSignalerContextWithCancel(ss.T(), context.Background())
   413  	ss.e.Start(ctx)
   414  	unittest.AssertClosesBefore(ss.T(), ss.e.Ready(), time.Second)
   415  	cancel()
   416  	unittest.AssertClosesBefore(ss.T(), ss.e.Done(), time.Second)
   417  }
   418  
   419  // TestProcessingMultipleItems tests that items are processed in async way
   420  func (ss *SyncSuite) TestProcessingMultipleItems() {
   421  	ctx, cancel := irrecoverable.NewMockSignalerContextWithCancel(ss.T(), context.Background())
   422  	ss.e.Start(ctx)
   423  	unittest.AssertClosesBefore(ss.T(), ss.e.Ready(), time.Second)
   424  	defer cancel()
   425  
   426  	originID := unittest.IdentifierFixture()
   427  	for i := 0; i < 5; i++ {
   428  		msg := &messages.SyncResponse{
   429  			Nonce:  uint64(i),
   430  			Height: uint64(1000 + i),
   431  		}
   432  		ss.core.On("HandleHeight", mock.Anything, msg.Height).Once()
   433  		require.NoError(ss.T(), ss.e.Process(channels.SyncCommittee, originID, msg))
   434  	}
   435  
   436  	finalHeight := ss.head.Height
   437  	for i := 0; i < 5; i++ {
   438  		msg := &messages.SyncRequest{
   439  			Nonce:  uint64(i),
   440  			Height: finalHeight - 100,
   441  		}
   442  
   443  		originID := unittest.IdentifierFixture()
   444  		ss.core.On("WithinTolerance", mock.Anything, mock.Anything).Return(false)
   445  		ss.core.On("HandleHeight", mock.Anything, msg.Height).Once()
   446  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil)
   447  
   448  		// misbehavior might or might not be reported
   449  		ss.con.On("ReportMisbehavior", mock.Anything).Return(mock.Anything).Maybe()
   450  
   451  		require.NoError(ss.T(), ss.e.Process(channels.SyncCommittee, originID, msg))
   452  	}
   453  
   454  	// give at least some time to process items
   455  	time.Sleep(time.Millisecond * 100)
   456  
   457  	ss.core.AssertExpectations(ss.T())
   458  }
   459  
   460  // TestProcessUnsupportedMessageType tests that Process and ProcessLocal correctly handle a case where invalid message type
   461  // was submitted from network layer.
   462  func (ss *SyncSuite) TestProcessUnsupportedMessageType() {
   463  	invalidEvent := uint64(42)
   464  	engines := []netint.MessageProcessor{ss.e, ss.e.requestHandler}
   465  	for _, e := range engines {
   466  		err := e.Process("ch", unittest.IdentifierFixture(), invalidEvent)
   467  		// shouldn't result in error since byzantine inputs are expected
   468  		require.NoError(ss.T(), err)
   469  	}
   470  }