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  }