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

     1  package synchronization
     2  
     3  import (
     4  	"io"
     5  	"math"
     6  	"math/rand"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/rs/zerolog"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/mock"
    13  	"github.com/stretchr/testify/require"
    14  	"github.com/stretchr/testify/suite"
    15  
    16  	"github.com/onflow/flow-go/engine"
    17  	mockcollection "github.com/onflow/flow-go/engine/collection/mock"
    18  	clustermodel "github.com/onflow/flow-go/model/cluster"
    19  	"github.com/onflow/flow-go/model/flow"
    20  	"github.com/onflow/flow-go/model/flow/filter"
    21  	"github.com/onflow/flow-go/model/messages"
    22  	"github.com/onflow/flow-go/module/chainsync"
    23  	synccore "github.com/onflow/flow-go/module/chainsync"
    24  	"github.com/onflow/flow-go/module/metrics"
    25  	module "github.com/onflow/flow-go/module/mock"
    26  	netint "github.com/onflow/flow-go/network"
    27  	"github.com/onflow/flow-go/network/channels"
    28  	"github.com/onflow/flow-go/network/mocknetwork"
    29  	clusterint "github.com/onflow/flow-go/state/cluster"
    30  	cluster "github.com/onflow/flow-go/state/cluster/mock"
    31  	storerr "github.com/onflow/flow-go/storage"
    32  	storage "github.com/onflow/flow-go/storage/mock"
    33  	"github.com/onflow/flow-go/utils/unittest"
    34  )
    35  
    36  func TestSyncEngine(t *testing.T) {
    37  	suite.Run(t, new(SyncSuite))
    38  }
    39  
    40  type SyncSuite struct {
    41  	suite.Suite
    42  	myID         flow.Identifier
    43  	participants flow.IdentityList
    44  	head         *flow.Header
    45  	heights      map[uint64]*clustermodel.Block
    46  	blockIDs     map[flow.Identifier]*clustermodel.Block
    47  	net          *mocknetwork.Network
    48  	con          *mocknetwork.Conduit
    49  	me           *module.Local
    50  	state        *cluster.State
    51  	snapshot     *cluster.Snapshot
    52  	params       *cluster.Params
    53  	blocks       *storage.ClusterBlocks
    54  	comp         *mockcollection.Compliance
    55  	core         *module.SyncCore
    56  	e            *Engine
    57  }
    58  
    59  func (ss *SyncSuite) SetupTest() {
    60  	// generate own ID
    61  	ss.participants = unittest.IdentityListFixture(3, unittest.WithRole(flow.RoleCollection))
    62  	ss.myID = ss.participants[0].NodeID
    63  
    64  	// generate a header for the final state
    65  	header := unittest.BlockHeaderFixture()
    66  	ss.head = header
    67  
    68  	// create maps to enable block returns
    69  	ss.heights = make(map[uint64]*clustermodel.Block)
    70  	ss.blockIDs = make(map[flow.Identifier]*clustermodel.Block)
    71  	clusterID := header.ChainID
    72  
    73  	// set up the network module mock
    74  	ss.net = &mocknetwork.Network{}
    75  	ss.net.On("Register", channels.SyncCluster(clusterID), mock.Anything).Return(
    76  		func(network channels.Channel, engine netint.MessageProcessor) netint.Conduit {
    77  			return ss.con
    78  		},
    79  		nil,
    80  	)
    81  
    82  	// set up the network conduit mock
    83  	ss.con = &mocknetwork.Conduit{}
    84  
    85  	// set up the local module mock
    86  	ss.me = &module.Local{}
    87  	ss.me.On("NodeID").Return(
    88  		func() flow.Identifier {
    89  			return ss.myID
    90  		},
    91  	)
    92  
    93  	// set up the protocol state mock
    94  	ss.state = &cluster.State{}
    95  	ss.state.On("Params").Return(
    96  		func() clusterint.Params {
    97  			return ss.params
    98  		},
    99  	)
   100  
   101  	ss.params = &cluster.Params{}
   102  	ss.params.On("ChainID").Return(ss.head.ChainID, nil)
   103  
   104  	ss.state.On("Final").Return(
   105  		func() clusterint.Snapshot {
   106  			return ss.snapshot
   107  		},
   108  	)
   109  
   110  	// set up the snapshot mock
   111  	ss.snapshot = &cluster.Snapshot{}
   112  	ss.snapshot.On("Head").Return(
   113  		func() *flow.Header {
   114  			return ss.head
   115  		},
   116  		nil,
   117  	)
   118  	ss.snapshot.On("Identities", mock.Anything).Return(
   119  		func(selector flow.IdentityFilter[flow.Identity]) flow.IdentityList {
   120  			return ss.participants.Filter(selector)
   121  		},
   122  		nil,
   123  	)
   124  
   125  	// set up blocks storage mock
   126  	ss.blocks = &storage.ClusterBlocks{}
   127  	ss.blocks.On("ByHeight", mock.Anything).Return(
   128  		func(height uint64) *clustermodel.Block {
   129  			return ss.heights[height]
   130  		},
   131  		func(height uint64) error {
   132  			_, enabled := ss.heights[height]
   133  			if !enabled {
   134  				return storerr.ErrNotFound
   135  			}
   136  			return nil
   137  		},
   138  	)
   139  	ss.blocks.On("ByID", mock.Anything).Return(
   140  		func(blockID flow.Identifier) *clustermodel.Block {
   141  			return ss.blockIDs[blockID]
   142  		},
   143  		func(blockID flow.Identifier) error {
   144  			_, enabled := ss.blockIDs[blockID]
   145  			if !enabled {
   146  				return storerr.ErrNotFound
   147  			}
   148  			return nil
   149  		},
   150  	)
   151  
   152  	// set up compliance engine mock
   153  	ss.comp = mockcollection.NewCompliance(ss.T())
   154  
   155  	// set up sync core
   156  	ss.core = &module.SyncCore{}
   157  
   158  	// initialize the engine
   159  	log := zerolog.New(io.Discard)
   160  	metrics := metrics.NewNoopCollector()
   161  
   162  	e, err := New(log, metrics, ss.net, ss.me, ss.participants.ToSkeleton(), ss.state, ss.blocks, ss.comp, ss.core)
   163  	require.NoError(ss.T(), err, "should pass engine initialization")
   164  
   165  	ss.e = e
   166  }
   167  
   168  func (ss *SyncSuite) TestOnSyncRequest() {
   169  
   170  	// generate origin and request message
   171  	originID := unittest.IdentifierFixture()
   172  	req := &messages.SyncRequest{
   173  		Nonce:  rand.Uint64(),
   174  		Height: 0,
   175  	}
   176  
   177  	// regardless of request height, if within tolerance, we should not respond
   178  	ss.core.On("HandleHeight", ss.head, req.Height)
   179  	ss.core.On("WithinTolerance", ss.head, req.Height).Return(true)
   180  	err := ss.e.requestHandler.onSyncRequest(originID, req)
   181  	ss.Assert().NoError(err, "same height sync request should pass")
   182  	ss.con.AssertNotCalled(ss.T(), "Unicast", mock.Anything, mock.Anything)
   183  
   184  	// if request height is higher than local finalized, we should not respond
   185  	req.Height = ss.head.Height + 1
   186  	ss.core.On("HandleHeight", ss.head, req.Height)
   187  	ss.core.On("WithinTolerance", ss.head, req.Height).Return(false)
   188  	err = ss.e.requestHandler.onSyncRequest(originID, req)
   189  	ss.Assert().NoError(err, "same height sync request should pass")
   190  	ss.con.AssertNotCalled(ss.T(), "Unicast", mock.Anything, mock.Anything)
   191  
   192  	// if the request height is lower than head and outside tolerance, we should submit correct response
   193  	req.Height = ss.head.Height - 1
   194  	ss.core.On("HandleHeight", ss.head, req.Height)
   195  	ss.core.On("WithinTolerance", ss.head, req.Height).Return(false)
   196  	ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Run(
   197  		func(args mock.Arguments) {
   198  			res := args.Get(0).(*messages.SyncResponse)
   199  			assert.Equal(ss.T(), ss.head.Height, res.Height, "response should contain head height")
   200  			assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   201  			recipientID := args.Get(1).(flow.Identifier)
   202  			assert.Equal(ss.T(), originID, recipientID, "should send response to original sender")
   203  		},
   204  	)
   205  	err = ss.e.requestHandler.onSyncRequest(originID, req)
   206  	require.NoError(ss.T(), err, "smaller height sync request should pass")
   207  
   208  	ss.core.AssertExpectations(ss.T())
   209  }
   210  
   211  func (ss *SyncSuite) TestOnSyncResponse() {
   212  
   213  	// generate origin ID and response message
   214  	originID := unittest.IdentifierFixture()
   215  	res := &messages.SyncResponse{
   216  		Nonce:  rand.Uint64(),
   217  		Height: rand.Uint64(),
   218  	}
   219  
   220  	// the height should be handled
   221  	ss.core.On("HandleHeight", ss.head, res.Height)
   222  	ss.e.onSyncResponse(originID, res)
   223  	ss.core.AssertExpectations(ss.T())
   224  }
   225  
   226  func (ss *SyncSuite) TestOnRangeRequest() {
   227  
   228  	// generate originID and range request
   229  	originID := unittest.IdentifierFixture()
   230  	req := &messages.RangeRequest{
   231  		Nonce:      rand.Uint64(),
   232  		FromHeight: 0,
   233  		ToHeight:   0,
   234  	}
   235  
   236  	// fill in blocks at heights -1 to -4 from head
   237  	ref := ss.head.Height
   238  	for height := ref; height >= ref-4; height-- {
   239  		block := unittest.ClusterBlockFixture()
   240  		block.Header.Height = height
   241  		ss.heights[height] = &block
   242  	}
   243  
   244  	// empty range should be a no-op
   245  	ss.T().Run("empty range", func(t *testing.T) {
   246  		req.FromHeight = ref
   247  		req.ToHeight = ref - 1
   248  		err := ss.e.requestHandler.onRangeRequest(originID, req)
   249  		require.NoError(ss.T(), err, "empty range request should pass")
   250  		ss.con.AssertNumberOfCalls(ss.T(), "Unicast", 0)
   251  	})
   252  
   253  	// range with only unknown block should be a no-op
   254  	ss.T().Run("range with unknown block", func(t *testing.T) {
   255  		req.FromHeight = ref + 1
   256  		req.ToHeight = ref + 3
   257  		err := ss.e.requestHandler.onRangeRequest(originID, req)
   258  		require.NoError(ss.T(), err, "unknown range request should pass")
   259  		ss.con.AssertNumberOfCalls(ss.T(), "Unicast", 0)
   260  	})
   261  
   262  	// a request for same from and to should send single block
   263  	ss.T().Run("from == to", func(t *testing.T) {
   264  		req.FromHeight = ref - 1
   265  		req.ToHeight = ref - 1
   266  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Once().Run(
   267  			func(args mock.Arguments) {
   268  				res := args.Get(0).(*messages.ClusterBlockResponse)
   269  				expected := ss.heights[ref-1]
   270  				actual := res.Blocks[0].ToInternal()
   271  				assert.Equal(ss.T(), expected.ID(), actual.ID(), "response should contain right block")
   272  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   273  				recipientID := args.Get(1).(flow.Identifier)
   274  				assert.Equal(ss.T(), originID, recipientID, "should send response to original requester")
   275  			},
   276  		)
   277  		err := ss.e.requestHandler.onRangeRequest(originID, req)
   278  		require.NoError(ss.T(), err, "range request with higher to height should pass")
   279  	})
   280  
   281  	// a request for a range that we partially have should send partial response
   282  	ss.T().Run("have partial range", func(t *testing.T) {
   283  		req.FromHeight = ref - 2
   284  		req.ToHeight = ref + 2
   285  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Once().Run(
   286  			func(args mock.Arguments) {
   287  				res := args.Get(0).(*messages.ClusterBlockResponse)
   288  				expected := []*clustermodel.Block{ss.heights[ref-2], ss.heights[ref-1], ss.heights[ref]}
   289  				assert.ElementsMatch(ss.T(), expected, res.BlocksInternal(), "response should contain right blocks")
   290  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   291  				recipientID := args.Get(1).(flow.Identifier)
   292  				assert.Equal(ss.T(), originID, recipientID, "should send response to original requester")
   293  			},
   294  		)
   295  		err := ss.e.requestHandler.onRangeRequest(originID, req)
   296  		require.NoError(ss.T(), err, "valid range with missing blocks should fail")
   297  	})
   298  
   299  	// a request for a range we entirely have should send all blocks
   300  	ss.T().Run("have entire range", func(t *testing.T) {
   301  		req.FromHeight = ref - 2
   302  		req.ToHeight = ref
   303  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Once().Run(
   304  			func(args mock.Arguments) {
   305  				res := args.Get(0).(*messages.ClusterBlockResponse)
   306  				expected := []*clustermodel.Block{ss.heights[ref-2], ss.heights[ref-1], ss.heights[ref]}
   307  				assert.ElementsMatch(ss.T(), expected, res.BlocksInternal(), "response should contain right blocks")
   308  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   309  				recipientID := args.Get(1).(flow.Identifier)
   310  				assert.Equal(ss.T(), originID, recipientID, "should send response to original requester")
   311  			},
   312  		)
   313  		err := ss.e.requestHandler.onRangeRequest(originID, req)
   314  		require.NoError(ss.T(), err, "valid range request should pass")
   315  	})
   316  
   317  	// a request for an oversized range should return blocks within range
   318  	ss.T().Run("oversized range", func(t *testing.T) {
   319  		// range should get reset to FromHeight to FromHeight + MaxSize (i.e. MaxSize+1 blocks)
   320  		req.FromHeight = ref - 4
   321  		req.ToHeight = math.MaxUint64
   322  
   323  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Once().Run(
   324  			func(args mock.Arguments) {
   325  				res := args.Get(0).(*messages.ClusterBlockResponse)
   326  				expected := []*clustermodel.Block{ss.heights[ref-4], ss.heights[ref-3], ss.heights[ref-2]}
   327  				assert.ElementsMatch(ss.T(), expected, res.BlocksInternal(), "response should contain right blocks")
   328  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   329  				recipientID := args.Get(1).(flow.Identifier)
   330  				assert.Equal(ss.T(), originID, recipientID, "should send response to original requester")
   331  			},
   332  		)
   333  
   334  		// Rebuild sync core with a smaller max size
   335  		var err error
   336  		config := chainsync.DefaultConfig()
   337  		config.MaxSize = 2
   338  		ss.e.requestHandler.core, err = chainsync.New(ss.e.log, config, metrics.NewNoopCollector(), flow.Localnet)
   339  		require.NoError(ss.T(), err)
   340  
   341  		err = ss.e.requestHandler.onRangeRequest(originID, req)
   342  		require.NoError(ss.T(), err, "valid range request should pass")
   343  	})
   344  }
   345  
   346  func (ss *SyncSuite) TestOnBatchRequest() {
   347  
   348  	// generate origin ID and batch request
   349  	originID := unittest.IdentifierFixture()
   350  	req := &messages.BatchRequest{
   351  		Nonce:    rand.Uint64(),
   352  		BlockIDs: nil,
   353  	}
   354  
   355  	// an empty request should not lead to response
   356  	ss.T().Run("empty request", func(t *testing.T) {
   357  		req.BlockIDs = []flow.Identifier{}
   358  		err := ss.e.requestHandler.onBatchRequest(originID, req)
   359  		require.NoError(ss.T(), err, "should pass empty request")
   360  		ss.con.AssertNumberOfCalls(ss.T(), "Unicast", 0)
   361  	})
   362  
   363  	// a non-empty request for missing block ID should be a no-op
   364  	ss.T().Run("request for missing blocks", func(t *testing.T) {
   365  		req.BlockIDs = unittest.IdentifierListFixture(1)
   366  		err := ss.e.requestHandler.onBatchRequest(originID, req)
   367  		require.NoError(ss.T(), err, "should pass request for missing block")
   368  		ss.con.AssertNumberOfCalls(ss.T(), "Unicast", 0)
   369  	})
   370  
   371  	// a non-empty request for existing block IDs should send right response
   372  	ss.T().Run("request for existing blocks", func(t *testing.T) {
   373  		block := unittest.ClusterBlockFixture()
   374  		block.Header.Height = ss.head.Height - 1
   375  		req.BlockIDs = []flow.Identifier{block.ID()}
   376  		ss.blockIDs[block.ID()] = &block
   377  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Once().Run(
   378  			func(args mock.Arguments) {
   379  				res := args.Get(0).(*messages.ClusterBlockResponse)
   380  				assert.Equal(ss.T(), &block, res.Blocks[0].ToInternal(), "response should contain right block")
   381  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   382  				recipientID := args.Get(1).(flow.Identifier)
   383  				assert.Equal(ss.T(), originID, recipientID, "response should be send to original requester")
   384  			},
   385  		)
   386  		err := ss.e.requestHandler.onBatchRequest(originID, req)
   387  		require.NoError(ss.T(), err, "should pass request with valid block")
   388  	})
   389  
   390  	// a request for an oversized batch should return MaxSize blocks
   391  	ss.T().Run("oversized range", func(t *testing.T) {
   392  		// setup request for 5 blocks. response should contain the first 2 (MaxSize)
   393  		ss.blockIDs = make(map[flow.Identifier]*clustermodel.Block)
   394  		req.BlockIDs = make([]flow.Identifier, 5)
   395  		for i := 0; i < len(req.BlockIDs); i++ {
   396  			b := unittest.ClusterBlockFixture()
   397  			b.Header.Height = ss.head.Height - uint64(i)
   398  			req.BlockIDs[i] = b.ID()
   399  			ss.blockIDs[b.ID()] = &b
   400  		}
   401  
   402  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil).Once().Run(
   403  			func(args mock.Arguments) {
   404  				res := args.Get(0).(*messages.ClusterBlockResponse)
   405  				assert.ElementsMatch(ss.T(), []*clustermodel.Block{ss.blockIDs[req.BlockIDs[0]], ss.blockIDs[req.BlockIDs[1]]}, res.BlocksInternal(), "response should contain right block")
   406  				assert.Equal(ss.T(), req.Nonce, res.Nonce, "response should contain request nonce")
   407  				recipientID := args.Get(1).(flow.Identifier)
   408  				assert.Equal(ss.T(), originID, recipientID, "response should be send to original requester")
   409  			},
   410  		)
   411  
   412  		// Rebuild sync core with a smaller max size
   413  		var err error
   414  		config := chainsync.DefaultConfig()
   415  		config.MaxSize = 2
   416  		ss.e.requestHandler.core, err = chainsync.New(ss.e.log, config, metrics.NewNoopCollector(), flow.Localnet)
   417  		require.NoError(ss.T(), err)
   418  
   419  		err = ss.e.requestHandler.onBatchRequest(originID, req)
   420  		require.NoError(ss.T(), err, "should pass request with valid block")
   421  	})
   422  }
   423  
   424  func (ss *SyncSuite) TestOnBlockResponse() {
   425  
   426  	// generate origin and block response
   427  	originID := unittest.IdentifierFixture()
   428  	res := &messages.ClusterBlockResponse{
   429  		Nonce:  rand.Uint64(),
   430  		Blocks: []messages.UntrustedClusterBlock{},
   431  	}
   432  
   433  	// add one block that should be processed
   434  	processable := unittest.ClusterBlockFixture()
   435  	ss.core.On("HandleBlock", processable.Header).Return(true)
   436  	res.Blocks = append(res.Blocks, messages.UntrustedClusterBlockFromInternal(&processable))
   437  
   438  	// add one block that should not be processed
   439  	unprocessable := unittest.ClusterBlockFixture()
   440  	ss.core.On("HandleBlock", unprocessable.Header).Return(false)
   441  	res.Blocks = append(res.Blocks, messages.UntrustedClusterBlockFromInternal(&unprocessable))
   442  
   443  	ss.comp.On("OnSyncedClusterBlock", mock.Anything).Run(func(args mock.Arguments) {
   444  		res := args.Get(0).(flow.Slashable[*messages.ClusterBlockProposal])
   445  		converted := res.Message.Block.ToInternal()
   446  		ss.Assert().Equal(processable.Header, converted.Header)
   447  		ss.Assert().Equal(processable.Payload, converted.Payload)
   448  		ss.Assert().Equal(originID, res.OriginID)
   449  	}).Return(nil)
   450  
   451  	ss.e.onBlockResponse(originID, res)
   452  	ss.comp.AssertExpectations(ss.T())
   453  	ss.core.AssertExpectations(ss.T())
   454  }
   455  
   456  func (ss *SyncSuite) TestPollHeight() {
   457  
   458  	// check that we send to three nodes from our total list
   459  	others := ss.participants.Filter(filter.HasNodeID[flow.Identity](ss.participants[1:].NodeIDs()...))
   460  	ss.con.On("Multicast", mock.Anything, synccore.DefaultPollNodes, others[0].NodeID, others[1].NodeID).Return(nil).Run(
   461  		func(args mock.Arguments) {
   462  			req := args.Get(0).(*messages.SyncRequest)
   463  			require.Equal(ss.T(), ss.head.Height, req.Height, "request should contain finalized height")
   464  		},
   465  	)
   466  	ss.e.pollHeight()
   467  	ss.con.AssertExpectations(ss.T())
   468  }
   469  
   470  func (ss *SyncSuite) TestSendRequests() {
   471  
   472  	ranges := unittest.RangeListFixture(1)
   473  	batches := unittest.BatchListFixture(1)
   474  
   475  	// should submit and mark requested all ranges
   476  	ss.con.On("Multicast", mock.AnythingOfType("*messages.RangeRequest"), synccore.DefaultBlockRequestNodes, mock.Anything, mock.Anything).Return(nil).Run(
   477  		func(args mock.Arguments) {
   478  			req := args.Get(0).(*messages.RangeRequest)
   479  			ss.Assert().Equal(ranges[0].From, req.FromHeight)
   480  			ss.Assert().Equal(ranges[0].To, req.ToHeight)
   481  		},
   482  	)
   483  	ss.core.On("RangeRequested", ranges[0])
   484  
   485  	// should submit and mark requested all batches
   486  	ss.con.On("Multicast", mock.AnythingOfType("*messages.BatchRequest"), synccore.DefaultBlockRequestNodes, mock.Anything, mock.Anything, mock.Anything).Return(nil).Run(
   487  		func(args mock.Arguments) {
   488  			req := args.Get(0).(*messages.BatchRequest)
   489  			ss.Assert().Equal(batches[0].BlockIDs, req.BlockIDs)
   490  		},
   491  	)
   492  	ss.core.On("BatchRequested", batches[0])
   493  
   494  	// exclude my node ID
   495  	ss.e.sendRequests(ranges, batches)
   496  	ss.con.AssertExpectations(ss.T())
   497  }
   498  
   499  // test a synchronization engine can be started and stopped
   500  func (ss *SyncSuite) TestStartStop() {
   501  	unittest.AssertReturnsBefore(ss.T(), func() {
   502  		<-ss.e.Ready()
   503  		<-ss.e.Done()
   504  	}, time.Second)
   505  }
   506  
   507  // TestProcessingMultipleItems tests that items are processed in async way
   508  func (ss *SyncSuite) TestProcessingMultipleItems() {
   509  	<-ss.e.Ready()
   510  
   511  	originID := unittest.IdentifierFixture()
   512  	for i := 0; i < 5; i++ {
   513  		msg := &messages.SyncResponse{
   514  			Nonce:  uint64(i),
   515  			Height: uint64(1000 + i),
   516  		}
   517  		ss.core.On("HandleHeight", mock.Anything, msg.Height).Once()
   518  		require.NoError(ss.T(), ss.e.Process(channels.SyncCommittee, originID, msg))
   519  	}
   520  
   521  	finalHeight := ss.head.Height
   522  	for i := 0; i < 5; i++ {
   523  		msg := &messages.SyncRequest{
   524  			Nonce:  uint64(i),
   525  			Height: finalHeight - 100,
   526  		}
   527  
   528  		originID := unittest.IdentifierFixture()
   529  		ss.core.On("WithinTolerance", mock.Anything, mock.Anything).Return(false).Once()
   530  		ss.core.On("HandleHeight", mock.Anything, msg.Height).Once()
   531  		ss.con.On("Unicast", mock.Anything, mock.Anything).Return(nil)
   532  
   533  		require.NoError(ss.T(), ss.e.Process(channels.SyncCommittee, originID, msg))
   534  	}
   535  
   536  	// give at least some time to process items
   537  	time.Sleep(time.Millisecond * 100)
   538  
   539  	ss.core.AssertExpectations(ss.T())
   540  }
   541  
   542  // TestProcessUnsupportedMessageType tests that Process and ProcessLocal correctly handle a case where invalid message type
   543  // was submitted from network layer.
   544  func (ss *SyncSuite) TestProcessUnsupportedMessageType() {
   545  	invalidEvent := uint64(42)
   546  	engines := []netint.Engine{ss.e, ss.e.requestHandler}
   547  	for _, e := range engines {
   548  		err := e.Process("ch", unittest.IdentifierFixture(), invalidEvent)
   549  		// shouldn't result in error since byzantine inputs are expected
   550  		require.NoError(ss.T(), err)
   551  		// in case of local processing error cannot be consumed since all inputs are trusted
   552  		err = e.ProcessLocal(invalidEvent)
   553  		require.Error(ss.T(), err)
   554  		require.True(ss.T(), engine.IsIncompatibleInputTypeError(err))
   555  	}
   556  }