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 }