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 }