github.com/onflow/flow-go@v0.33.17/engine/common/requester/engine_test.go (about)

     1  package requester
     2  
     3  import (
     4  	"math/rand"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/rs/zerolog"
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/mock"
    11  	"github.com/stretchr/testify/require"
    12  	"github.com/vmihailenco/msgpack"
    13  	"go.uber.org/atomic"
    14  
    15  	module "github.com/onflow/flow-go/module/mock"
    16  
    17  	"github.com/onflow/flow-go/engine"
    18  	"github.com/onflow/flow-go/model/flow"
    19  	"github.com/onflow/flow-go/model/flow/filter"
    20  	"github.com/onflow/flow-go/model/messages"
    21  	"github.com/onflow/flow-go/module/metrics"
    22  	"github.com/onflow/flow-go/network/mocknetwork"
    23  	protocol "github.com/onflow/flow-go/state/protocol/mock"
    24  	"github.com/onflow/flow-go/utils/unittest"
    25  )
    26  
    27  func TestEntityByID(t *testing.T) {
    28  
    29  	request := Engine{
    30  		unit:  engine.NewUnit(),
    31  		items: make(map[flow.Identifier]*Item),
    32  	}
    33  
    34  	now := time.Now().UTC()
    35  
    36  	entityID := unittest.IdentifierFixture()
    37  	selector := filter.Any
    38  	request.EntityByID(entityID, selector)
    39  
    40  	assert.Len(t, request.items, 1)
    41  	item, contains := request.items[entityID]
    42  	if assert.True(t, contains) {
    43  		assert.Equal(t, item.EntityID, entityID)
    44  		assert.Equal(t, item.NumAttempts, uint(0))
    45  		cutoff := item.LastRequested.Add(item.RetryAfter)
    46  		assert.True(t, cutoff.Before(now)) // make sure we push out immediately
    47  	}
    48  }
    49  
    50  func TestDispatchRequestVarious(t *testing.T) {
    51  
    52  	identities := unittest.IdentityListFixture(16)
    53  	targetID := identities[0].NodeID
    54  
    55  	final := &protocol.Snapshot{}
    56  	final.On("Identities", mock.Anything).Return(
    57  		func(selector flow.IdentityFilter) flow.IdentityList {
    58  			return identities.Filter(selector)
    59  		},
    60  		nil,
    61  	)
    62  
    63  	state := &protocol.State{}
    64  	state.On("Final").Return(final)
    65  
    66  	cfg := Config{
    67  		BatchInterval:  200 * time.Millisecond,
    68  		BatchThreshold: 999,
    69  		RetryInitial:   100 * time.Millisecond,
    70  		RetryFunction:  RetryLinear(10 * time.Millisecond),
    71  		RetryAttempts:  2,
    72  		RetryMaximum:   300 * time.Millisecond,
    73  	}
    74  
    75  	// item that has just been added, should be included
    76  	justAdded := &Item{
    77  		EntityID:      unittest.IdentifierFixture(),
    78  		NumAttempts:   0,
    79  		LastRequested: time.Time{},
    80  		RetryAfter:    cfg.RetryInitial,
    81  		ExtraSelector: filter.Any,
    82  	}
    83  
    84  	// item was tried long time ago, should be included
    85  	triedAnciently := &Item{
    86  		EntityID:      unittest.IdentifierFixture(),
    87  		NumAttempts:   1,
    88  		LastRequested: time.Now().UTC().Add(-cfg.RetryMaximum),
    89  		RetryAfter:    cfg.RetryFunction(cfg.RetryInitial),
    90  		ExtraSelector: filter.Any,
    91  	}
    92  
    93  	// item that was just tried, should be excluded
    94  	triedRecently := &Item{
    95  		EntityID:      unittest.IdentifierFixture(),
    96  		NumAttempts:   1,
    97  		LastRequested: time.Now().UTC(),
    98  		RetryAfter:    cfg.RetryFunction(cfg.RetryInitial),
    99  	}
   100  
   101  	// item was tried twice, should be excluded
   102  	triedTwice := &Item{
   103  		EntityID:      unittest.IdentifierFixture(),
   104  		NumAttempts:   2,
   105  		LastRequested: time.Time{},
   106  		RetryAfter:    cfg.RetryInitial,
   107  		ExtraSelector: filter.Any,
   108  	}
   109  
   110  	items := make(map[flow.Identifier]*Item)
   111  	items[justAdded.EntityID] = justAdded
   112  	items[triedAnciently.EntityID] = triedAnciently
   113  	items[triedRecently.EntityID] = triedRecently
   114  	items[triedTwice.EntityID] = triedTwice
   115  
   116  	var nonce uint64
   117  
   118  	con := &mocknetwork.Conduit{}
   119  	con.On("Unicast", mock.Anything, mock.Anything).Run(
   120  		func(args mock.Arguments) {
   121  			request := args.Get(0).(*messages.EntityRequest)
   122  			originID := args.Get(1).(flow.Identifier)
   123  			nonce = request.Nonce
   124  			assert.Equal(t, originID, targetID)
   125  			assert.ElementsMatch(t, request.EntityIDs, []flow.Identifier{justAdded.EntityID, triedAnciently.EntityID})
   126  		},
   127  	).Return(nil)
   128  
   129  	request := Engine{
   130  		unit:     engine.NewUnit(),
   131  		metrics:  metrics.NewNoopCollector(),
   132  		cfg:      cfg,
   133  		state:    state,
   134  		con:      con,
   135  		items:    items,
   136  		requests: make(map[uint64]*messages.EntityRequest),
   137  		selector: filter.HasNodeID(targetID),
   138  	}
   139  	dispatched, err := request.dispatchRequest()
   140  	require.NoError(t, err)
   141  	require.True(t, dispatched)
   142  
   143  	con.AssertExpectations(t)
   144  
   145  	request.unit.Lock()
   146  	assert.Contains(t, request.requests, nonce)
   147  	request.unit.Unlock()
   148  
   149  	// TODO: racy/slow test
   150  	time.Sleep(2 * cfg.RetryInitial)
   151  
   152  	request.unit.Lock()
   153  	assert.NotContains(t, request.requests, nonce)
   154  	request.unit.Unlock()
   155  }
   156  
   157  func TestDispatchRequestBatchSize(t *testing.T) {
   158  
   159  	batchLimit := uint(16)
   160  	totalItems := uint(99)
   161  
   162  	identities := unittest.IdentityListFixture(16)
   163  
   164  	final := &protocol.Snapshot{}
   165  	final.On("Identities", mock.Anything).Return(
   166  		func(selector flow.IdentityFilter) flow.IdentityList {
   167  			return identities.Filter(selector)
   168  		},
   169  		nil,
   170  	)
   171  
   172  	state := &protocol.State{}
   173  	state.On("Final").Return(final)
   174  
   175  	cfg := Config{
   176  		BatchInterval:  24 * time.Hour,
   177  		BatchThreshold: batchLimit,
   178  		RetryInitial:   24 * time.Hour,
   179  		RetryFunction:  RetryLinear(1),
   180  		RetryAttempts:  1,
   181  		RetryMaximum:   24 * time.Hour,
   182  	}
   183  
   184  	// item that has just been added, should be included
   185  	items := make(map[flow.Identifier]*Item)
   186  	for i := uint(0); i < totalItems; i++ {
   187  		item := &Item{
   188  			EntityID:      unittest.IdentifierFixture(),
   189  			NumAttempts:   0,
   190  			LastRequested: time.Time{},
   191  			RetryAfter:    cfg.RetryInitial,
   192  			ExtraSelector: filter.Any,
   193  		}
   194  		items[item.EntityID] = item
   195  	}
   196  
   197  	con := &mocknetwork.Conduit{}
   198  	con.On("Unicast", mock.Anything, mock.Anything).Run(
   199  		func(args mock.Arguments) {
   200  			request := args.Get(0).(*messages.EntityRequest)
   201  			assert.Len(t, request.EntityIDs, int(batchLimit))
   202  		},
   203  	).Return(nil)
   204  
   205  	request := Engine{
   206  		unit:     engine.NewUnit(),
   207  		metrics:  metrics.NewNoopCollector(),
   208  		cfg:      cfg,
   209  		state:    state,
   210  		con:      con,
   211  		items:    items,
   212  		requests: make(map[uint64]*messages.EntityRequest),
   213  		selector: filter.Any,
   214  	}
   215  	dispatched, err := request.dispatchRequest()
   216  	require.NoError(t, err)
   217  	require.True(t, dispatched)
   218  
   219  	con.AssertExpectations(t)
   220  }
   221  
   222  func TestOnEntityResponseValid(t *testing.T) {
   223  
   224  	identities := unittest.IdentityListFixture(16)
   225  	targetID := identities[0].NodeID
   226  
   227  	final := &protocol.Snapshot{}
   228  	final.On("Identities", mock.Anything).Return(
   229  		func(selector flow.IdentityFilter) flow.IdentityList {
   230  			return identities.Filter(selector)
   231  		},
   232  		nil,
   233  	)
   234  
   235  	state := &protocol.State{}
   236  	state.On("Final").Return(final)
   237  
   238  	nonce := rand.Uint64()
   239  
   240  	wanted1 := unittest.CollectionFixture(1)
   241  	wanted2 := unittest.CollectionFixture(2)
   242  	unavailable := unittest.CollectionFixture(3)
   243  	unwanted := unittest.CollectionFixture(4)
   244  
   245  	now := time.Now()
   246  
   247  	iwanted1 := &Item{
   248  		EntityID:      wanted1.ID(),
   249  		LastRequested: now,
   250  		ExtraSelector: filter.Any,
   251  	}
   252  	iwanted2 := &Item{
   253  		EntityID:      wanted2.ID(),
   254  		LastRequested: now,
   255  		ExtraSelector: filter.Any,
   256  	}
   257  	iunavailable := &Item{
   258  		EntityID:      unavailable.ID(),
   259  		LastRequested: now,
   260  		ExtraSelector: filter.Any,
   261  	}
   262  
   263  	bwanted1, _ := msgpack.Marshal(wanted1)
   264  	bwanted2, _ := msgpack.Marshal(wanted2)
   265  	bunwanted, _ := msgpack.Marshal(unwanted)
   266  
   267  	res := &messages.EntityResponse{
   268  		Nonce:     nonce,
   269  		EntityIDs: []flow.Identifier{wanted1.ID(), wanted2.ID(), unwanted.ID()},
   270  		Blobs:     [][]byte{bwanted1, bwanted2, bunwanted},
   271  	}
   272  
   273  	req := &messages.EntityRequest{
   274  		Nonce:     nonce,
   275  		EntityIDs: []flow.Identifier{wanted1.ID(), wanted2.ID(), unavailable.ID()},
   276  	}
   277  
   278  	done := make(chan struct{})
   279  	called := *atomic.NewUint64(0)
   280  	request := Engine{
   281  		unit:     engine.NewUnit(),
   282  		metrics:  metrics.NewNoopCollector(),
   283  		state:    state,
   284  		items:    make(map[flow.Identifier]*Item),
   285  		requests: make(map[uint64]*messages.EntityRequest),
   286  		selector: filter.HasNodeID(targetID),
   287  		create:   func() flow.Entity { return &flow.Collection{} },
   288  		handle: func(flow.Identifier, flow.Entity) {
   289  			if called.Inc() >= 2 {
   290  				close(done)
   291  			}
   292  		},
   293  	}
   294  
   295  	request.items[iwanted1.EntityID] = iwanted1
   296  	request.items[iwanted2.EntityID] = iwanted2
   297  	request.items[iunavailable.EntityID] = iunavailable
   298  
   299  	request.requests[req.Nonce] = req
   300  
   301  	err := request.onEntityResponse(targetID, res)
   302  	assert.NoError(t, err)
   303  
   304  	// check that the request was removed
   305  	assert.NotContains(t, request.requests, nonce)
   306  
   307  	// check that the provided items were removed
   308  	assert.NotContains(t, request.items, wanted1.ID())
   309  	assert.NotContains(t, request.items, wanted2.ID())
   310  
   311  	// check that the missing item is still there
   312  	assert.Contains(t, request.items, unavailable.ID())
   313  
   314  	// make sure we processed two items
   315  	unittest.AssertClosesBefore(t, done, time.Second)
   316  
   317  	// check that the missing items timestamp was reset
   318  	assert.Equal(t, iunavailable.LastRequested, time.Time{})
   319  }
   320  
   321  func TestOnEntityIntegrityCheck(t *testing.T) {
   322  	identities := unittest.IdentityListFixture(16)
   323  	targetID := identities[0].NodeID
   324  
   325  	final := &protocol.Snapshot{}
   326  	final.On("Identities", mock.Anything).Return(
   327  		func(selector flow.IdentityFilter) flow.IdentityList {
   328  			return identities.Filter(selector)
   329  		},
   330  		nil,
   331  	)
   332  
   333  	state := &protocol.State{}
   334  	state.On("Final").Return(final)
   335  
   336  	nonce := rand.Uint64()
   337  
   338  	wanted := unittest.CollectionFixture(1)
   339  	wanted2 := unittest.CollectionFixture(2)
   340  
   341  	now := time.Now()
   342  
   343  	iwanted := &Item{
   344  		EntityID:       wanted.ID(),
   345  		LastRequested:  now,
   346  		ExtraSelector:  filter.Any,
   347  		checkIntegrity: true,
   348  	}
   349  
   350  	assert.NotEqual(t, wanted, wanted2)
   351  
   352  	// prepare payload from different entity
   353  	bwanted, _ := msgpack.Marshal(wanted2)
   354  
   355  	res := &messages.EntityResponse{
   356  		Nonce:     nonce,
   357  		EntityIDs: []flow.Identifier{wanted.ID()},
   358  		Blobs:     [][]byte{bwanted},
   359  	}
   360  
   361  	req := &messages.EntityRequest{
   362  		Nonce:     nonce,
   363  		EntityIDs: []flow.Identifier{wanted.ID()},
   364  	}
   365  
   366  	called := make(chan struct{})
   367  	request := Engine{
   368  		unit:     engine.NewUnit(),
   369  		metrics:  metrics.NewNoopCollector(),
   370  		state:    state,
   371  		items:    make(map[flow.Identifier]*Item),
   372  		requests: make(map[uint64]*messages.EntityRequest),
   373  		selector: filter.HasNodeID(targetID),
   374  		create:   func() flow.Entity { return &flow.Collection{} },
   375  		handle:   func(flow.Identifier, flow.Entity) { close(called) },
   376  	}
   377  
   378  	request.items[iwanted.EntityID] = iwanted
   379  
   380  	request.requests[req.Nonce] = req
   381  
   382  	err := request.onEntityResponse(targetID, res)
   383  	assert.NoError(t, err)
   384  
   385  	// check that the request was removed
   386  	assert.NotContains(t, request.requests, nonce)
   387  
   388  	// check that the provided item wasn't removed
   389  	assert.Contains(t, request.items, wanted.ID())
   390  
   391  	iwanted.checkIntegrity = false
   392  	request.items[iwanted.EntityID] = iwanted
   393  	request.requests[req.Nonce] = req
   394  
   395  	err = request.onEntityResponse(targetID, res)
   396  	assert.NoError(t, err)
   397  
   398  	// make sure we process item without checking integrity
   399  	unittest.AssertClosesBefore(t, called, time.Second)
   400  }
   401  
   402  // Verify that the origin should not be checked when ValidateStaking config is set to false
   403  func TestOriginValidation(t *testing.T) {
   404  	identities := unittest.IdentityListFixture(16)
   405  	targetID := identities[0].NodeID
   406  	wrongID := identities[1].NodeID
   407  	meID := identities[3].NodeID
   408  
   409  	final := &protocol.Snapshot{}
   410  	final.On("Identities", mock.Anything).Return(
   411  		func(selector flow.IdentityFilter) flow.IdentityList {
   412  			return identities.Filter(selector)
   413  		},
   414  		nil,
   415  	)
   416  
   417  	state := &protocol.State{}
   418  	state.On("Final").Return(final)
   419  
   420  	me := &module.Local{}
   421  
   422  	me.On("NodeID").Return(meID)
   423  
   424  	nonce := rand.Uint64()
   425  
   426  	wanted := unittest.CollectionFixture(1)
   427  
   428  	now := time.Now()
   429  
   430  	iwanted := &Item{
   431  		EntityID:       wanted.ID(),
   432  		LastRequested:  now,
   433  		ExtraSelector:  filter.HasNodeID(targetID),
   434  		checkIntegrity: true,
   435  	}
   436  
   437  	// prepare payload
   438  	bwanted, _ := msgpack.Marshal(wanted)
   439  
   440  	res := &messages.EntityResponse{
   441  		Nonce:     nonce,
   442  		EntityIDs: []flow.Identifier{wanted.ID()},
   443  		Blobs:     [][]byte{bwanted},
   444  	}
   445  
   446  	req := &messages.EntityRequest{
   447  		Nonce:     nonce,
   448  		EntityIDs: []flow.Identifier{wanted.ID()},
   449  	}
   450  
   451  	network := &mocknetwork.Network{}
   452  	network.On("Register", mock.Anything, mock.Anything).Return(nil, nil)
   453  
   454  	e, err := New(
   455  		zerolog.Nop(),
   456  		metrics.NewNoopCollector(),
   457  		network,
   458  		me,
   459  		state,
   460  		"",
   461  		filter.HasNodeID(targetID),
   462  		func() flow.Entity { return &flow.Collection{} },
   463  	)
   464  	assert.NoError(t, err)
   465  
   466  	called := make(chan struct{})
   467  
   468  	e.WithHandle(func(origin flow.Identifier, _ flow.Entity) {
   469  		// we expect wrong origin to propagate here with validation disabled
   470  		assert.Equal(t, wrongID, origin)
   471  		close(called)
   472  	})
   473  
   474  	e.items[iwanted.EntityID] = iwanted
   475  	e.requests[req.Nonce] = req
   476  
   477  	err = e.onEntityResponse(wrongID, res)
   478  	assert.Error(t, err)
   479  	assert.IsType(t, engine.InvalidInputError{}, err)
   480  
   481  	e.cfg.ValidateStaking = false
   482  
   483  	err = e.onEntityResponse(wrongID, res)
   484  	assert.NoError(t, err)
   485  
   486  	// handler are called async, but this should be extremely quick
   487  	unittest.AssertClosesBefore(t, called, time.Second)
   488  }