github.com/koko1123/flow-go-1@v0.29.6/module/chainsync/core_test.go (about) 1 package chainsync 2 3 import ( 4 "fmt" 5 "io" 6 "math/rand" 7 "testing" 8 "time" 9 10 "github.com/rs/zerolog" 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 "github.com/stretchr/testify/suite" 14 15 "github.com/koko1123/flow-go-1/model/chainsync" 16 "github.com/koko1123/flow-go-1/model/flow" 17 "github.com/koko1123/flow-go-1/module/metrics" 18 "github.com/koko1123/flow-go-1/utils/unittest" 19 ) 20 21 func TestSyncCore(t *testing.T) { 22 suite.Run(t, new(SyncSuite)) 23 } 24 25 type SyncSuite struct { 26 suite.Suite 27 core *Core 28 } 29 30 func (ss *SyncSuite) SetupTest() { 31 var err error 32 33 ss.core, err = New(zerolog.New(io.Discard), DefaultConfig(), metrics.NewNoopCollector()) 34 ss.Require().Nil(err) 35 } 36 37 func (ss *SyncSuite) QueuedStatus() *chainsync.Status { 38 return &chainsync.Status{ 39 Queued: time.Now(), 40 } 41 } 42 43 func (ss *SyncSuite) RequestedStatus() *chainsync.Status { 44 return &chainsync.Status{ 45 Queued: time.Now().Add(-time.Second), 46 Requested: time.Now(), 47 Attempts: 1, 48 } 49 } 50 51 func (ss *SyncSuite) ReceivedStatus(header *flow.Header) *chainsync.Status { 52 return &chainsync.Status{ 53 BlockHeight: header.Height, 54 Queued: time.Now().Add(-time.Second * 2), 55 Requested: time.Now().Add(-time.Second), 56 Attempts: 1, 57 Header: header, 58 Received: time.Now(), 59 } 60 } 61 62 func (ss *SyncSuite) TestQueueByHeight() { 63 64 // generate a number of heights 65 var heights []uint64 66 for n := 0; n < 100; n++ { 67 heights = append(heights, rand.Uint64()) 68 } 69 70 // add all of them to engine 71 for _, height := range heights { 72 ss.core.queueByHeight(height) 73 } 74 75 // check they are all in the map now 76 for _, height := range heights { 77 status, exists := ss.core.heights[height] 78 ss.Assert().True(exists, "status map should contain the block ID") 79 ss.Assert().False(status.WasRequested(), "block should have correct status") 80 } 81 82 // get current count and add all again 83 count := len(ss.core.heights) 84 for _, height := range heights { 85 ss.core.queueByHeight(height) 86 } 87 88 // check that operation was idempotent (size still the same) 89 assert.Len(ss.T(), ss.core.heights, count, "height map should be the same") 90 91 } 92 93 func (ss *SyncSuite) TestQueueByBlockID() { 94 95 // generate a number of block IDs 96 var blockIDs []flow.Identifier 97 for n := 0; n < 100; n++ { 98 blockIDs = append(blockIDs, unittest.IdentifierFixture()) 99 } 100 101 // add all of them to engine 102 for _, blockID := range blockIDs { 103 ss.core.queueByBlockID(blockID, 0) 104 } 105 106 // check they are all in the map now 107 for _, blockID := range blockIDs { 108 status, exists := ss.core.blockIDs[blockID] 109 ss.Assert().True(exists, "status map should contain the block ID") 110 ss.Assert().False(status.WasRequested(), "block should have correct status") 111 } 112 113 // get current count and add all again 114 count := len(ss.core.blockIDs) 115 for _, blockID := range blockIDs { 116 ss.core.queueByBlockID(blockID, 0) 117 } 118 119 // check that operation was idempotent (size still the same) 120 assert.Len(ss.T(), ss.core.blockIDs, count, "block ID map should be the same") 121 } 122 123 func (ss *SyncSuite) TestRequestBlock() { 124 125 queuedID := unittest.IdentifierFixture() 126 requestedID := unittest.IdentifierFixture() 127 received := unittest.BlockHeaderFixture() 128 129 ss.core.blockIDs[queuedID] = ss.QueuedStatus() 130 ss.core.blockIDs[requestedID] = ss.RequestedStatus() 131 ss.core.blockIDs[received.ID()] = ss.RequestedStatus() 132 133 // queued status should stay the same 134 ss.core.RequestBlock(queuedID, 0) 135 assert.True(ss.T(), ss.core.blockIDs[queuedID].WasQueued()) 136 137 // requested status should stay the same 138 ss.core.RequestBlock(requestedID, 0) 139 assert.True(ss.T(), ss.core.blockIDs[requestedID].WasRequested()) 140 141 // received status should be re-queued by ID 142 ss.core.RequestBlock(received.ID(), 0) 143 assert.True(ss.T(), ss.core.blockIDs[received.ID()].WasQueued()) 144 assert.False(ss.T(), ss.core.blockIDs[received.ID()].WasReceived()) 145 assert.False(ss.T(), ss.core.heights[received.Height].WasQueued()) 146 } 147 148 func (ss *SyncSuite) TestHandleBlock() { 149 150 unrequested := unittest.BlockHeaderFixture() 151 queuedByHeight := unittest.BlockHeaderFixture() 152 ss.core.heights[queuedByHeight.Height] = ss.QueuedStatus() 153 requestedByID := unittest.BlockHeaderFixture() 154 ss.core.blockIDs[requestedByID.ID()] = ss.RequestedStatus() 155 received := unittest.BlockHeaderFixture() 156 ss.core.heights[received.Height] = ss.ReceivedStatus(received) 157 ss.core.blockIDs[received.ID()] = ss.ReceivedStatus(received) 158 159 // should ignore un-requested blocks 160 shouldProcess := ss.core.HandleBlock(unrequested) 161 ss.Assert().False(shouldProcess, "should not process un-requested block") 162 ss.Assert().NotContains(ss.core.heights, unrequested.Height) 163 ss.Assert().NotContains(ss.core.blockIDs, unrequested.ID()) 164 165 // should mark queued blocks as received, and process them 166 shouldProcess = ss.core.HandleBlock(queuedByHeight) 167 ss.Assert().True(shouldProcess, "should process queued block") 168 ss.Assert().True(ss.core.blockIDs[queuedByHeight.ID()].WasReceived(), "status should be reflected in block ID map") 169 ss.Assert().True(ss.core.heights[queuedByHeight.Height].WasReceived(), "status should be reflected in height map") 170 171 // should mark requested block as received, and process them 172 shouldProcess = ss.core.HandleBlock(requestedByID) 173 ss.Assert().True(shouldProcess, "should process requested block") 174 ss.Assert().True(ss.core.blockIDs[requestedByID.ID()].WasReceived(), "status should be reflected in block ID map") 175 ss.Assert().True(ss.core.heights[requestedByID.Height].WasReceived(), "status should be reflected in height map") 176 177 // should leave received blocks, and not process them 178 shouldProcess = ss.core.HandleBlock(received) 179 ss.Assert().False(shouldProcess, "should not process already received block") 180 ss.Assert().True(ss.core.blockIDs[received.ID()].WasReceived(), "status should remain reflected in block ID map") 181 ss.Assert().True(ss.core.heights[received.Height].WasReceived(), "status should remain reflected in height map") 182 } 183 184 func (ss *SyncSuite) TestHandleHeight() { 185 186 final := unittest.BlockHeaderFixture() 187 lower := final.Height - uint64(ss.core.Config.Tolerance) 188 aboveWithinTolerance := final.Height + 1 189 aboveOutsideTolerance := final.Height + uint64(ss.core.Config.Tolerance+1) 190 191 // a height lower than finalized should be a no-op 192 ss.core.HandleHeight(final, lower) 193 ss.Assert().Len(ss.core.heights, 0) 194 195 // a height higher than finalized, but within tolerance, should be a no-op 196 ss.core.HandleHeight(final, aboveWithinTolerance) 197 ss.Assert().Len(ss.core.heights, 0) 198 199 // a height higher than finalized and outside tolerance should queue missing heights 200 ss.core.HandleHeight(final, aboveOutsideTolerance) 201 ss.Assert().Len(ss.core.heights, int(aboveOutsideTolerance-final.Height)) 202 for height := final.Height + 1; height <= aboveOutsideTolerance; height++ { 203 ss.Assert().Contains(ss.core.heights, height) 204 } 205 } 206 207 func (ss *SyncSuite) TestGetRequestableItems() { 208 209 // get current timestamp and zero timestamp 210 now := time.Now().UTC() 211 zero := time.Time{} 212 213 // fill in a height status that should be skipped 214 skipHeight := uint64(rand.Uint64()) 215 ss.core.heights[skipHeight] = &chainsync.Status{ 216 Queued: now, 217 Requested: now, 218 Attempts: 0, 219 } 220 221 // fill in a height status that should be deleted 222 dropHeight := uint64(rand.Uint64()) 223 ss.core.heights[dropHeight] = &chainsync.Status{ 224 Queued: now, 225 Requested: zero, 226 Attempts: ss.core.Config.MaxAttempts, 227 } 228 229 // fill in a height status that should be requested 230 reqHeight := uint64(rand.Uint64()) 231 ss.core.heights[reqHeight] = &chainsync.Status{ 232 Queued: now, 233 Requested: zero, 234 Attempts: 0, 235 } 236 237 // fill in a block ID that should be skipped 238 skipBlockID := unittest.IdentifierFixture() 239 ss.core.blockIDs[skipBlockID] = &chainsync.Status{ 240 Queued: now, 241 Requested: now, 242 Attempts: 0, 243 } 244 245 // fill in a block ID that should be deleted 246 dropBlockID := unittest.IdentifierFixture() 247 ss.core.blockIDs[dropBlockID] = &chainsync.Status{ 248 Queued: now, 249 Requested: zero, 250 Attempts: ss.core.Config.MaxAttempts, 251 } 252 253 // fill in a block ID that should be requested 254 reqBlockID := unittest.IdentifierFixture() 255 ss.core.blockIDs[reqBlockID] = &chainsync.Status{ 256 Queued: now, 257 Requested: zero, 258 Attempts: 0, 259 } 260 261 // execute the pending scan 262 heights, blockIDs := ss.core.getRequestableItems() 263 264 // check only the request height is in heights 265 require.NotContains(ss.T(), heights, skipHeight, "output should not contain skip height") 266 require.NotContains(ss.T(), heights, dropHeight, "output should not contain drop height") 267 require.Contains(ss.T(), heights, reqHeight, "output should contain request height") 268 269 // check only the request block ID is in block IDs 270 require.NotContains(ss.T(), blockIDs, skipBlockID, "output should not contain skip blockID") 271 require.NotContains(ss.T(), blockIDs, dropBlockID, "output should not contain drop blockID") 272 require.Contains(ss.T(), blockIDs, reqBlockID, "output should contain request blockID") 273 274 // check only delete height was deleted 275 require.Contains(ss.T(), ss.core.heights, skipHeight, "status should not contain skip height") 276 require.NotContains(ss.T(), ss.core.heights, dropHeight, "status should not contain drop height") 277 require.Contains(ss.T(), ss.core.heights, reqHeight, "status should contain request height") 278 279 // check only the delete block ID was deleted 280 require.Contains(ss.T(), ss.core.blockIDs, skipBlockID, "status should not contain skip blockID") 281 require.NotContains(ss.T(), ss.core.blockIDs, dropBlockID, "status should not contain drop blockID") 282 require.Contains(ss.T(), ss.core.blockIDs, reqBlockID, "status should contain request blockID") 283 } 284 285 func (ss *SyncSuite) TestGetRanges() { 286 287 // use a small max request size for simpler test cases 288 ss.core.Config.MaxSize = 4 289 290 ss.Run("contiguous", func() { 291 input := []uint64{1, 2, 3, 4, 5, 6, 7, 8} 292 expected := []chainsync.Range{{From: 1, To: 4}, {From: 5, To: 8}} 293 ranges := ss.core.getRanges(input) 294 ss.Assert().Equal(expected, ranges) 295 }) 296 297 ss.Run("non-contiguous", func() { 298 input := []uint64{1, 3} 299 expected := []chainsync.Range{{From: 1, To: 1}, {From: 3, To: 3}} 300 ranges := ss.core.getRanges(input) 301 ss.Assert().Equal(expected, ranges) 302 }) 303 304 ss.Run("with dupes", func() { 305 input := []uint64{1, 2, 2, 3, 3, 4} 306 expected := []chainsync.Range{{From: 1, To: 4}} 307 ranges := ss.core.getRanges(input) 308 ss.Assert().Equal(expected, ranges) 309 }) 310 } 311 312 func (ss *SyncSuite) TestGetBatches() { 313 314 // use a small max request size for simpler test cases 315 ss.core.Config.MaxSize = 4 316 317 ss.Run("less than max size", func() { 318 input := unittest.IdentifierListFixture(2) 319 expected := []chainsync.Batch{{BlockIDs: input}} 320 batches := ss.core.getBatches(input) 321 ss.Assert().Equal(expected, batches) 322 }) 323 324 ss.Run("greater than max size", func() { 325 input := unittest.IdentifierListFixture(6) 326 expected := []chainsync.Batch{{BlockIDs: input[:4]}, {BlockIDs: input[4:]}} 327 batches := ss.core.getBatches(input) 328 ss.Assert().Equal(expected, batches) 329 }) 330 } 331 332 func (ss *SyncSuite) TestSelectRequests() { 333 334 ss.core.Config.MaxRequests = 4 335 336 type testcase struct { 337 // number of candidate ranges and batches 338 nRanges, nBatches int 339 // number of each request that should be selected 340 expectedNRanges, expectedNBatches int 341 } 342 343 cases := []testcase{ 344 { 345 nRanges: 4, 346 nBatches: 1, 347 expectedNRanges: 4, 348 expectedNBatches: 0, 349 }, { 350 nRanges: 5, 351 nBatches: 1, 352 expectedNRanges: 4, 353 expectedNBatches: 0, 354 }, { 355 nRanges: 3, 356 nBatches: 1, 357 expectedNRanges: 3, 358 expectedNBatches: 1, 359 }, { 360 nRanges: 0, 361 nBatches: 1, 362 expectedNRanges: 0, 363 expectedNBatches: 1, 364 }, { 365 nRanges: 0, 366 nBatches: 5, 367 expectedNRanges: 0, 368 expectedNBatches: 4, 369 }, 370 } 371 372 for _, tcase := range cases { 373 ss.Run(fmt.Sprintf("%d ranges / %d batches", tcase.nRanges, tcase.nBatches), func() { 374 inputRanges := unittest.RangeListFixture(tcase.nRanges) 375 inputBatches := unittest.BatchListFixture(tcase.nBatches) 376 377 ranges, batches := ss.core.selectRequests(inputRanges, inputBatches) 378 ss.Assert().Len(ranges, tcase.expectedNRanges) 379 if tcase.expectedNRanges > 0 { 380 ss.Assert().Equal(ranges, inputRanges[:tcase.expectedNRanges]) 381 } 382 ss.Assert().Len(batches, tcase.expectedNBatches) 383 if tcase.expectedNBatches > 0 { 384 ss.Assert().Equal(batches, inputBatches[:tcase.expectedNBatches]) 385 } 386 }) 387 } 388 } 389 390 func (ss *SyncSuite) TestPrune() { 391 392 // our latest finalized height is 100 393 final := unittest.BlockHeaderFixture() 394 final.Height = 100 395 396 var ( 397 prunableHeights []flow.Block 398 prunableBlockIDs []flow.Block 399 unprunable []flow.Block 400 ) 401 402 // add some finalized blocks by height 403 for i := 0; i < 3; i++ { 404 block := unittest.BlockFixture() 405 block.Header.Height = uint64(i + 1) 406 ss.core.heights[block.Header.Height] = ss.QueuedStatus() 407 prunableHeights = append(prunableHeights, block) 408 } 409 // add some un-finalized blocks by height 410 for i := 0; i < 3; i++ { 411 block := unittest.BlockFixture() 412 block.Header.Height = final.Height + uint64(i+1) 413 ss.core.heights[block.Header.Height] = ss.QueuedStatus() 414 unprunable = append(unprunable, block) 415 } 416 417 // add some finalized blocks by block ID 418 for i := 0; i < 3; i++ { 419 block := unittest.BlockFixture() 420 block.Header.Height = uint64(i + 1) 421 ss.core.blockIDs[block.ID()] = ss.ReceivedStatus(block.Header) 422 prunableBlockIDs = append(prunableBlockIDs, block) 423 } 424 // add some un-finalized, received blocks by block ID 425 for i := 0; i < 3; i++ { 426 block := unittest.BlockFixture() 427 block.Header.Height = 100 + uint64(i+1) 428 ss.core.blockIDs[block.ID()] = ss.ReceivedStatus(block.Header) 429 unprunable = append(unprunable, block) 430 } 431 432 heightsBefore := len(ss.core.heights) 433 blockIDsBefore := len(ss.core.blockIDs) 434 435 // prune the pending requests 436 ss.core.prune(final) 437 438 assert.Equal(ss.T(), heightsBefore-len(prunableHeights), len(ss.core.heights)) 439 assert.Equal(ss.T(), blockIDsBefore-len(prunableBlockIDs), len(ss.core.blockIDs)) 440 441 // ensure the right things were pruned 442 for _, block := range prunableBlockIDs { 443 _, exists := ss.core.blockIDs[block.ID()] 444 assert.False(ss.T(), exists) 445 } 446 for _, block := range prunableHeights { 447 _, exists := ss.core.heights[block.Header.Height] 448 assert.False(ss.T(), exists) 449 } 450 for _, block := range unprunable { 451 _, heightExists := ss.core.heights[block.Header.Height] 452 _, blockIDExists := ss.core.blockIDs[block.ID()] 453 assert.True(ss.T(), heightExists || blockIDExists) 454 } 455 }