github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/pacemaker/view_tracker_test.go (about)

     1  package pacemaker
     2  
     3  import (
     4  	"errors"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/mock"
     8  	"github.com/stretchr/testify/require"
     9  	"github.com/stretchr/testify/suite"
    10  
    11  	"github.com/onflow/flow-go/consensus/hotstuff"
    12  	"github.com/onflow/flow-go/consensus/hotstuff/helper"
    13  	"github.com/onflow/flow-go/consensus/hotstuff/mocks"
    14  	"github.com/onflow/flow-go/model/flow"
    15  )
    16  
    17  func TestViewTracker(t *testing.T) {
    18  	suite.Run(t, new(ViewTrackerTestSuite))
    19  }
    20  
    21  type ViewTrackerTestSuite struct {
    22  	suite.Suite
    23  
    24  	initialView uint64
    25  	initialQC   *flow.QuorumCertificate
    26  	initialTC   *flow.TimeoutCertificate
    27  
    28  	livenessData *hotstuff.LivenessData // Caution: we hand the memory address to viewTracker, which could modify this
    29  	persist      *mocks.Persister
    30  	tracker      viewTracker
    31  }
    32  
    33  func (s *ViewTrackerTestSuite) SetupTest() {
    34  	s.initialView = 5
    35  	s.initialQC = helper.MakeQC(helper.WithQCView(4))
    36  	s.initialTC = nil
    37  
    38  	s.livenessData = &hotstuff.LivenessData{
    39  		NewestQC:    s.initialQC,
    40  		LastViewTC:  s.initialTC,
    41  		CurrentView: s.initialView, // we entered view 5 by observing a QC for view 4
    42  	}
    43  	s.persist = mocks.NewPersister(s.T())
    44  	s.persist.On("GetLivenessData").Return(s.livenessData, nil).Once()
    45  
    46  	var err error
    47  	s.tracker, err = newViewTracker(s.persist)
    48  	require.NoError(s.T(), err)
    49  }
    50  
    51  // confirmResultingState asserts that the view tracker's stored LivenessData reflects the provided
    52  // current view, newest QC, and last view TC.
    53  func (s *ViewTrackerTestSuite) confirmResultingState(curView uint64, qc *flow.QuorumCertificate, tc *flow.TimeoutCertificate) {
    54  	require.Equal(s.T(), curView, s.tracker.CurView())
    55  	require.Equal(s.T(), qc, s.tracker.NewestQC())
    56  	if tc == nil {
    57  		require.Nil(s.T(), s.tracker.LastViewTC())
    58  	} else {
    59  		require.Equal(s.T(), tc, s.tracker.LastViewTC())
    60  	}
    61  }
    62  
    63  // TestProcessQC_SkipIncreaseViewThroughQC tests that viewTracker increases view when receiving QC,
    64  // if applicable, by skipping views
    65  func (s *ViewTrackerTestSuite) TestProcessQC_SkipIncreaseViewThroughQC() {
    66  	// seeing a QC for the current view should advance the view by one
    67  	qc := QC(s.initialView)
    68  	expectedResultingView := s.initialView + 1
    69  	s.persist.On("PutLivenessData", LivenessData(qc)).Return(nil).Once()
    70  	resultingCurView, err := s.tracker.ProcessQC(qc)
    71  	require.NoError(s.T(), err)
    72  	require.Equal(s.T(), expectedResultingView, resultingCurView)
    73  	s.confirmResultingState(expectedResultingView, qc, nil)
    74  
    75  	// seeing a QC for 10 views in the future should advance to view +11
    76  	curView := s.tracker.CurView()
    77  	qc = QC(curView + 10)
    78  	expectedResultingView = curView + 11
    79  	s.persist.On("PutLivenessData", LivenessData(qc)).Return(nil).Once()
    80  	resultingCurView, err = s.tracker.ProcessQC(qc)
    81  	require.NoError(s.T(), err)
    82  	require.Equal(s.T(), expectedResultingView, resultingCurView)
    83  	s.confirmResultingState(expectedResultingView, qc, nil)
    84  }
    85  
    86  // TestProcessTC_SkipIncreaseViewThroughTC tests that viewTracker increases view when receiving TC,
    87  // if applicable, by skipping views
    88  func (s *ViewTrackerTestSuite) TestProcessTC_SkipIncreaseViewThroughTC() {
    89  	// seeing a TC for the current view should advance the view by one
    90  	qc := s.initialQC
    91  	tc := helper.MakeTC(helper.WithTCView(s.initialView), helper.WithTCNewestQC(qc))
    92  	expectedResultingView := s.initialView + 1
    93  	expectedLivenessData := &hotstuff.LivenessData{
    94  		CurrentView: expectedResultingView,
    95  		LastViewTC:  tc,
    96  		NewestQC:    qc,
    97  	}
    98  	s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once()
    99  	resultingCurView, err := s.tracker.ProcessTC(tc)
   100  	require.NoError(s.T(), err)
   101  	require.Equal(s.T(), expectedResultingView, resultingCurView)
   102  	s.confirmResultingState(expectedResultingView, qc, tc)
   103  
   104  	// seeing a TC for 10 views in the future should advance to view +11
   105  	curView := s.tracker.CurView()
   106  	tc = helper.MakeTC(helper.WithTCView(curView+10), helper.WithTCNewestQC(qc))
   107  	expectedResultingView = curView + 11
   108  	expectedLivenessData = &hotstuff.LivenessData{
   109  		CurrentView: expectedResultingView,
   110  		LastViewTC:  tc,
   111  		NewestQC:    qc,
   112  	}
   113  	s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once()
   114  	resultingCurView, err = s.tracker.ProcessTC(tc)
   115  	require.NoError(s.T(), err)
   116  	require.Equal(s.T(), expectedResultingView, resultingCurView)
   117  	s.confirmResultingState(expectedResultingView, qc, tc)
   118  }
   119  
   120  // TestProcessTC_IgnoreOldTC tests that viewTracker ignores old TC and doesn't advance round.
   121  func (s *ViewTrackerTestSuite) TestProcessTC_IgnoreOldTC() {
   122  	curView := s.tracker.CurView()
   123  	tc := helper.MakeTC(
   124  		helper.WithTCView(curView-1),
   125  		helper.WithTCNewestQC(QC(curView-2)))
   126  	resultingCurView, err := s.tracker.ProcessTC(tc)
   127  	require.NoError(s.T(), err)
   128  	require.Equal(s.T(), curView, resultingCurView)
   129  	s.confirmResultingState(curView, s.initialQC, s.initialTC)
   130  }
   131  
   132  // TestProcessTC_IgnoreNilTC tests that viewTracker accepts nil TC as allowed input but doesn't trigger a new view event
   133  func (s *ViewTrackerTestSuite) TestProcessTC_IgnoreNilTC() {
   134  	curView := s.tracker.CurView()
   135  	resultingCurView, err := s.tracker.ProcessTC(nil)
   136  	require.NoError(s.T(), err)
   137  	require.Equal(s.T(), curView, resultingCurView)
   138  	s.confirmResultingState(curView, s.initialQC, s.initialTC)
   139  }
   140  
   141  // TestProcessQC_PersistException tests that viewTracker propagates exception
   142  // when processing QC
   143  func (s *ViewTrackerTestSuite) TestProcessQC_PersistException() {
   144  	qc := QC(s.initialView)
   145  	exception := errors.New("persist-exception")
   146  	s.persist.On("PutLivenessData", mock.Anything).Return(exception).Once()
   147  
   148  	_, err := s.tracker.ProcessQC(qc)
   149  	require.ErrorIs(s.T(), err, exception)
   150  }
   151  
   152  // TestProcessTC_PersistException tests that viewTracker propagates exception
   153  // when processing TC
   154  func (s *ViewTrackerTestSuite) TestProcessTC_PersistException() {
   155  	tc := helper.MakeTC(helper.WithTCView(s.initialView))
   156  	exception := errors.New("persist-exception")
   157  	s.persist.On("PutLivenessData", mock.Anything).Return(exception).Once()
   158  
   159  	_, err := s.tracker.ProcessTC(tc)
   160  	require.ErrorIs(s.T(), err, exception)
   161  }
   162  
   163  // TestProcessQC_InvalidatesLastViewTC verifies that viewTracker does not retain any old
   164  // TC if the last view change was triggered by observing a QC from the previous view.
   165  func (s *ViewTrackerTestSuite) TestProcessQC_InvalidatesLastViewTC() {
   166  	initialView := s.tracker.CurView()
   167  	tc := helper.MakeTC(helper.WithTCView(initialView),
   168  		helper.WithTCNewestQC(s.initialQC))
   169  	s.persist.On("PutLivenessData", mock.Anything).Return(nil).Twice()
   170  	resultingCurView, err := s.tracker.ProcessTC(tc)
   171  	require.NoError(s.T(), err)
   172  	require.Equal(s.T(), initialView+1, resultingCurView)
   173  	require.NotNil(s.T(), s.tracker.LastViewTC())
   174  
   175  	qc := QC(initialView + 1)
   176  	resultingCurView, err = s.tracker.ProcessQC(qc)
   177  	require.NoError(s.T(), err)
   178  	require.Equal(s.T(), initialView+2, resultingCurView)
   179  	require.Nil(s.T(), s.tracker.LastViewTC())
   180  }
   181  
   182  // TestProcessQC_IgnoreOldQC tests that viewTracker ignores old QC and doesn't advance round
   183  func (s *ViewTrackerTestSuite) TestProcessQC_IgnoreOldQC() {
   184  	qc := QC(s.initialView - 1)
   185  	resultingCurView, err := s.tracker.ProcessQC(qc)
   186  	require.NoError(s.T(), err)
   187  	require.Equal(s.T(), s.initialView, resultingCurView)
   188  	s.confirmResultingState(s.initialView, s.initialQC, s.initialTC)
   189  }
   190  
   191  // TestProcessQC_UpdateNewestQC tests that viewTracker tracks the newest QC even if it has advanced past this view.
   192  // The only one scenario, where it is possible to receive a QC for a view that we already has passed, yet this QC
   193  // being newer than any known one is:
   194  //   - We advance views via TC.
   195  //   - A QC for a passed view that is newer than any known one can arrive in 3 ways:
   196  //     1. A QC (e.g. from the vote aggregator)
   197  //     2. A QC embedded into a TC, where the TC is for a passed view
   198  //     3. A QC embedded into a TC, where the TC is for the current or newer view
   199  func (s *ViewTrackerTestSuite) TestProcessQC_UpdateNewestQC() {
   200  	// Setup
   201  	// * we start in view 5
   202  	// * newest known QC is for view 4
   203  	// * we receive a TC for view 55, which results in entering view 56
   204  	initialView := s.tracker.CurView() //
   205  	tc := helper.MakeTC(helper.WithTCView(initialView+50), helper.WithTCNewestQC(s.initialQC))
   206  	s.persist.On("PutLivenessData", mock.Anything).Return(nil).Once()
   207  	expectedView := uint64(56) // processing the TC should results in entering view 56
   208  	resultingCurView, err := s.tracker.ProcessTC(tc)
   209  	require.NoError(s.T(), err)
   210  	require.Equal(s.T(), expectedView, resultingCurView)
   211  	s.confirmResultingState(expectedView, s.initialQC, tc)
   212  
   213  	// Test 1: add QC for view 9, which is newer than our initial QC - it should become our newest QC
   214  	qc := QC(s.tracker.NewestQC().View + 2)
   215  	expectedLivenessData := &hotstuff.LivenessData{
   216  		CurrentView: expectedView,
   217  		LastViewTC:  tc,
   218  		NewestQC:    qc,
   219  	}
   220  	s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once()
   221  	resultingCurView, err = s.tracker.ProcessQC(qc)
   222  	require.NoError(s.T(), err)
   223  	require.Equal(s.T(), expectedView, resultingCurView)
   224  	s.confirmResultingState(expectedView, qc, tc)
   225  
   226  	// Test 2: receiving a TC for a passed view, but the embedded QC is newer than the one we know
   227  	qc2 := QC(s.tracker.NewestQC().View + 4)
   228  	olderTC := helper.MakeTC(helper.WithTCView(qc2.View+3), helper.WithTCNewestQC(qc2))
   229  	expectedLivenessData = &hotstuff.LivenessData{
   230  		CurrentView: expectedView,
   231  		LastViewTC:  tc,
   232  		NewestQC:    qc2,
   233  	}
   234  	s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once()
   235  	resultingCurView, err = s.tracker.ProcessTC(olderTC)
   236  	require.NoError(s.T(), err)
   237  	require.Equal(s.T(), expectedView, resultingCurView)
   238  	s.confirmResultingState(expectedView, qc2, tc)
   239  
   240  	// Test 3: receiving a TC for a newer view, the embedded QC is newer than the one we know, but still for a passed view
   241  	qc3 := QC(s.tracker.NewestQC().View + 7)
   242  	finalView := expectedView + 1
   243  	newestTC := helper.MakeTC(helper.WithTCView(expectedView), helper.WithTCNewestQC(qc3))
   244  	expectedLivenessData = &hotstuff.LivenessData{
   245  		CurrentView: finalView,
   246  		LastViewTC:  newestTC,
   247  		NewestQC:    qc3,
   248  	}
   249  	s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once()
   250  	resultingCurView, err = s.tracker.ProcessTC(newestTC)
   251  	require.NoError(s.T(), err)
   252  	require.Equal(s.T(), finalView, resultingCurView)
   253  	s.confirmResultingState(finalView, qc3, newestTC)
   254  }