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 }