github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/timeoutcollector/timeout_collector_test.go (about)

     1  package timeoutcollector
     2  
     3  import (
     4  	"errors"
     5  	"math/rand"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/mock"
    11  	"github.com/stretchr/testify/require"
    12  	"github.com/stretchr/testify/suite"
    13  
    14  	"github.com/onflow/flow-go/consensus/hotstuff/helper"
    15  	"github.com/onflow/flow-go/consensus/hotstuff/mocks"
    16  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    17  	"github.com/onflow/flow-go/model/flow"
    18  	"github.com/onflow/flow-go/utils/unittest"
    19  )
    20  
    21  func TestTimeoutCollector(t *testing.T) {
    22  	suite.Run(t, new(TimeoutCollectorTestSuite))
    23  }
    24  
    25  // TimeoutCollectorTestSuite is a test suite for testing TimeoutCollector. It stores mocked
    26  // state internally for testing behavior.
    27  type TimeoutCollectorTestSuite struct {
    28  	suite.Suite
    29  
    30  	view      uint64
    31  	notifier  *mocks.TimeoutAggregationConsumer
    32  	processor *mocks.TimeoutProcessor
    33  	collector *TimeoutCollector
    34  }
    35  
    36  func (s *TimeoutCollectorTestSuite) SetupTest() {
    37  	s.view = 1000
    38  	s.notifier = mocks.NewTimeoutAggregationConsumer(s.T())
    39  	s.processor = mocks.NewTimeoutProcessor(s.T())
    40  
    41  	s.notifier.On("OnNewQcDiscovered", mock.Anything).Maybe()
    42  	s.notifier.On("OnNewTcDiscovered", mock.Anything).Maybe()
    43  
    44  	s.collector = NewTimeoutCollector(unittest.Logger(), s.view, s.notifier, s.processor)
    45  }
    46  
    47  // TestView tests that `View` returns the same value that was passed in constructor
    48  func (s *TimeoutCollectorTestSuite) TestView() {
    49  	require.Equal(s.T(), s.view, s.collector.View())
    50  }
    51  
    52  // TestAddTimeout_HappyPath tests that process in happy path executed by multiple workers deliver expected results
    53  // all operations should be successful, no errors expected
    54  func (s *TimeoutCollectorTestSuite) TestAddTimeout_HappyPath() {
    55  	var wg sync.WaitGroup
    56  	for i := 0; i < 20; i++ {
    57  		wg.Add(1)
    58  		go func() {
    59  			defer wg.Done()
    60  			timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view))
    61  			s.notifier.On("OnTimeoutProcessed", timeout).Once()
    62  			s.processor.On("Process", timeout).Return(nil).Once()
    63  			err := s.collector.AddTimeout(timeout)
    64  			require.NoError(s.T(), err)
    65  		}()
    66  	}
    67  
    68  	unittest.AssertReturnsBefore(s.T(), wg.Wait, time.Second)
    69  	s.processor.AssertExpectations(s.T())
    70  }
    71  
    72  // TestAddTimeout_DoubleTimeout tests that submitting two different timeouts for same view ends with reporting
    73  // double timeout to notifier which can be slashed later.
    74  func (s *TimeoutCollectorTestSuite) TestAddTimeout_DoubleTimeout() {
    75  	timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view))
    76  	s.notifier.On("OnTimeoutProcessed", timeout).Once()
    77  	s.processor.On("Process", timeout).Return(nil).Once()
    78  	err := s.collector.AddTimeout(timeout)
    79  	require.NoError(s.T(), err)
    80  
    81  	otherTimeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view),
    82  		helper.WithTimeoutObjectSignerID(timeout.SignerID))
    83  
    84  	s.notifier.On("OnDoubleTimeoutDetected", timeout, otherTimeout).Once()
    85  
    86  	err = s.collector.AddTimeout(otherTimeout)
    87  	require.NoError(s.T(), err)
    88  	s.notifier.AssertExpectations(s.T())
    89  	s.processor.AssertNumberOfCalls(s.T(), "Process", 1)
    90  }
    91  
    92  // TestAddTimeout_RepeatedTimeout checks that repeated timeouts are silently dropped without any errors.
    93  func (s *TimeoutCollectorTestSuite) TestAddTimeout_RepeatedTimeout() {
    94  	timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view))
    95  	s.notifier.On("OnTimeoutProcessed", timeout).Once()
    96  	s.processor.On("Process", timeout).Return(nil).Once()
    97  	err := s.collector.AddTimeout(timeout)
    98  	require.NoError(s.T(), err)
    99  	err = s.collector.AddTimeout(timeout)
   100  	require.NoError(s.T(), err)
   101  	s.processor.AssertNumberOfCalls(s.T(), "Process", 1)
   102  }
   103  
   104  // TestAddTimeout_TimeoutCacheException tests that submitting timeout object for view which is not designated for this
   105  // collector results in ErrTimeoutForIncompatibleView.
   106  func (s *TimeoutCollectorTestSuite) TestAddTimeout_TimeoutCacheException() {
   107  	// incompatible view is an exception and not handled by timeout collector
   108  	timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view + 1))
   109  	err := s.collector.AddTimeout(timeout)
   110  	require.ErrorIs(s.T(), err, ErrTimeoutForIncompatibleView)
   111  	s.processor.AssertNotCalled(s.T(), "Process")
   112  }
   113  
   114  // TestAddTimeout_InvalidTimeout tests that sentinel errors while processing timeouts are correctly handled and reported
   115  // to notifier, but exceptions are propagated to caller.
   116  func (s *TimeoutCollectorTestSuite) TestAddTimeout_InvalidTimeout() {
   117  	s.Run("invalid-timeout", func() {
   118  		timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view))
   119  		s.processor.On("Process", timeout).Return(model.NewInvalidTimeoutErrorf(timeout, "")).Once()
   120  		s.notifier.On("OnInvalidTimeoutDetected", mock.Anything).Run(func(args mock.Arguments) {
   121  			invalidTimeoutErr := args.Get(0).(model.InvalidTimeoutError)
   122  			require.Equal(s.T(), timeout, invalidTimeoutErr.Timeout)
   123  		}).Once()
   124  		err := s.collector.AddTimeout(timeout)
   125  		require.NoError(s.T(), err)
   126  
   127  		s.notifier.AssertCalled(s.T(), "OnInvalidTimeoutDetected", mock.Anything)
   128  	})
   129  	s.Run("process-exception", func() {
   130  		exception := errors.New("invalid-signature")
   131  		timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(s.view))
   132  		s.processor.On("Process", timeout).Return(exception).Once()
   133  		err := s.collector.AddTimeout(timeout)
   134  		require.ErrorIs(s.T(), err, exception)
   135  	})
   136  }
   137  
   138  // TestAddTimeout_TONotifications tests that TimeoutCollector in happy path reports the newest discovered QC and TC
   139  func (s *TimeoutCollectorTestSuite) TestAddTimeout_TONotifications() {
   140  	qcCount := 100
   141  	// generate QCs with increasing view numbers
   142  	if s.view < uint64(qcCount) {
   143  		s.T().Fatal("invalid test configuration")
   144  	}
   145  
   146  	*s.notifier = *mocks.NewTimeoutAggregationConsumer(s.T())
   147  
   148  	var highestReportedQC *flow.QuorumCertificate
   149  	s.notifier.On("OnNewQcDiscovered", mock.Anything).Run(func(args mock.Arguments) {
   150  		qc := args.Get(0).(*flow.QuorumCertificate)
   151  		if highestReportedQC == nil || highestReportedQC.View < qc.View {
   152  			highestReportedQC = qc
   153  		}
   154  	})
   155  
   156  	lastViewTC := helper.MakeTC(helper.WithTCView(s.view - 1))
   157  	s.notifier.On("OnNewTcDiscovered", lastViewTC).Once()
   158  
   159  	timeouts := make([]*model.TimeoutObject, 0, qcCount)
   160  	for i := 0; i < qcCount; i++ {
   161  		qc := helper.MakeQC(helper.WithQCView(uint64(i)))
   162  		timeout := helper.TimeoutObjectFixture(func(timeout *model.TimeoutObject) {
   163  			timeout.View = s.view
   164  			timeout.NewestQC = qc
   165  			timeout.LastViewTC = lastViewTC
   166  		})
   167  		timeouts = append(timeouts, timeout)
   168  		s.notifier.On("OnTimeoutProcessed", timeout).Once()
   169  		s.processor.On("Process", timeout).Return(nil).Once()
   170  	}
   171  
   172  	expectedHighestQC := timeouts[len(timeouts)-1].NewestQC
   173  
   174  	// shuffle timeouts in random order
   175  	rand.Shuffle(len(timeouts), func(i, j int) {
   176  		timeouts[i], timeouts[j] = timeouts[j], timeouts[i]
   177  	})
   178  
   179  	var wg sync.WaitGroup
   180  	wg.Add(len(timeouts))
   181  	for _, timeout := range timeouts {
   182  		go func(timeout *model.TimeoutObject) {
   183  			defer wg.Done()
   184  			err := s.collector.AddTimeout(timeout)
   185  			require.NoError(s.T(), err)
   186  		}(timeout)
   187  	}
   188  	wg.Wait()
   189  
   190  	require.Equal(s.T(), expectedHighestQC, highestReportedQC)
   191  }