github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/epochs/epoch_lookup_test.go (about) 1 package epochs 2 3 import ( 4 "context" 5 "sync" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 "github.com/stretchr/testify/suite" 12 13 "github.com/onflow/flow-go/consensus/hotstuff/model" 14 "github.com/onflow/flow-go/model/flow" 15 "github.com/onflow/flow-go/module/irrecoverable" 16 "github.com/onflow/flow-go/state/protocol" 17 mockprotocol "github.com/onflow/flow-go/state/protocol/mock" 18 "github.com/onflow/flow-go/utils/unittest" 19 "github.com/onflow/flow-go/utils/unittest/mocks" 20 ) 21 22 type EpochLookupSuite struct { 23 suite.Suite 24 25 // mocks 26 epochQuery *mocks.EpochQuery 27 state *mockprotocol.State 28 snapshot *mockprotocol.Snapshot 29 params *mockprotocol.Params 30 31 // backend for mocked functions 32 mu sync.Mutex // protects access to epochFallbackTriggered and phase 33 epochFallbackTriggered bool 34 phase flow.EpochPhase 35 36 // config for each epoch 37 currentEpochCounter uint64 38 prevEpoch epochRange 39 currEpoch epochRange 40 nextEpoch epochRange 41 42 lookup *EpochLookup 43 cancel context.CancelFunc 44 } 45 46 func TestEpochLookup(t *testing.T) { 47 suite.Run(t, new(EpochLookupSuite)) 48 } 49 50 func (suite *EpochLookupSuite) SetupTest() { 51 suite.currentEpochCounter = uint64(1) 52 suite.phase = flow.EpochPhaseStaking 53 54 suite.prevEpoch = epochRange{counter: suite.currentEpochCounter - 1, firstView: 100, finalView: 199} 55 suite.currEpoch = epochRange{counter: suite.currentEpochCounter, firstView: 200, finalView: 299} 56 suite.nextEpoch = epochRange{counter: suite.currentEpochCounter + 1, firstView: 300, finalView: 399} 57 58 suite.state = new(mockprotocol.State) 59 suite.snapshot = new(mockprotocol.Snapshot) 60 suite.params = new(mockprotocol.Params) 61 suite.epochQuery = mocks.NewEpochQuery(suite.T(), suite.currentEpochCounter) 62 63 suite.snapshot.On("Epochs").Return(suite.epochQuery) 64 suite.snapshot.On("Phase").Return( 65 func() flow.EpochPhase { return suite.Phase() }, 66 func() error { return nil }) 67 68 suite.params.On("EpochFallbackTriggered").Return( 69 func() bool { return suite.EpochFallbackTriggered() }, 70 func() error { return nil }) 71 72 suite.state.On("Final").Return(suite.snapshot) 73 suite.state.On("Params").Return(suite.params) 74 } 75 76 func (suite *EpochLookupSuite) TearDownTest() { 77 if suite.cancel != nil { 78 suite.cancel() 79 } 80 } 81 82 // WithLock runs the given function while holding the suite lock. Must be used 83 // while updating fields used as backends for mocked functions. 84 func (suite *EpochLookupSuite) WithLock(f func()) { 85 suite.mu.Lock() 86 f() 87 suite.mu.Unlock() 88 } 89 90 func (suite *EpochLookupSuite) EpochFallbackTriggered() bool { 91 suite.mu.Lock() 92 defer suite.mu.Unlock() 93 return suite.epochFallbackTriggered 94 } 95 96 func (suite *EpochLookupSuite) Phase() flow.EpochPhase { 97 suite.mu.Lock() 98 defer suite.mu.Unlock() 99 return suite.phase 100 } 101 102 // CommitEpochs adds the new epochs to the state. 103 func (suite *EpochLookupSuite) CommitEpochs(epochs ...epochRange) { 104 for _, epoch := range epochs { 105 mockEpoch := newMockEpoch(epoch.counter, epoch.firstView, epoch.finalView) 106 suite.epochQuery.Add(mockEpoch) 107 // if we add a next epoch (counter 1 greater than current), then set phase to committed 108 if epoch.counter == suite.currentEpochCounter+1 { 109 suite.WithLock(func() { 110 suite.phase = flow.EpochPhaseCommitted 111 }) 112 } 113 } 114 } 115 116 // CreateAndStartEpochLookup instantiates and starts the lookup. 117 // Should be called only once per test, after initial epoch mocks are created. 118 // It spawns a goroutine to detect fatal errors from the committee's error channel. 119 func (suite *EpochLookupSuite) CreateAndStartEpochLookup() { 120 lookup, err := NewEpochLookup(suite.state) 121 suite.Require().NoError(err) 122 ctx, cancel, errCh := irrecoverable.WithSignallerAndCancel(context.Background()) 123 lookup.Start(ctx) 124 go unittest.FailOnIrrecoverableError(suite.T(), ctx.Done(), errCh) 125 126 suite.lookup = lookup 127 suite.cancel = cancel 128 } 129 130 // TestEpochForViewWithFallback_Curr tests constructing and subsequently querying 131 // EpochLookup with an initial state of a current epoch. 132 func (suite *EpochLookupSuite) TestEpochForViewWithFallback_Curr() { 133 epochs := []epochRange{suite.currEpoch} 134 suite.CommitEpochs(epochs...) 135 suite.CreateAndStartEpochLookup() 136 testEpochForViewWithFallback(suite.T(), suite.lookup, suite.state, epochs...) 137 } 138 139 // TestEpochForViewWithFallback_PrevCurr tests constructing and subsequently querying 140 // EpochLookup with an initial state of a previous and current epoch. 141 func (suite *EpochLookupSuite) TestEpochForViewWithFallback_PrevCurr() { 142 epochs := []epochRange{suite.prevEpoch, suite.currEpoch} 143 suite.CommitEpochs(epochs...) 144 suite.CreateAndStartEpochLookup() 145 testEpochForViewWithFallback(suite.T(), suite.lookup, suite.state, epochs...) 146 } 147 148 // TestEpochForViewWithFallback_CurrNext tests constructing and subsequently querying 149 // EpochLookup with an initial state of a current and next epoch. 150 func (suite *EpochLookupSuite) TestEpochForViewWithFallback_CurrNext() { 151 epochs := []epochRange{suite.currEpoch, suite.nextEpoch} 152 suite.CommitEpochs(epochs...) 153 suite.CreateAndStartEpochLookup() 154 testEpochForViewWithFallback(suite.T(), suite.lookup, suite.state, epochs...) 155 } 156 157 // TestEpochForViewWithFallback_CurrNextPrev tests constructing and subsequently querying 158 // EpochLookup with an initial state of a previous, current, and next epoch. 159 func (suite *EpochLookupSuite) TestEpochForViewWithFallback_CurrNextPrev() { 160 epochs := []epochRange{suite.prevEpoch, suite.currEpoch, suite.nextEpoch} 161 suite.CommitEpochs(epochs...) 162 suite.CreateAndStartEpochLookup() 163 testEpochForViewWithFallback(suite.T(), suite.lookup, suite.state, epochs...) 164 } 165 166 // TestEpochForViewWithFallback_EpochFallbackTriggered tests constructing and subsequently querying 167 // EpochLookup with an initial state of epoch fallback triggered. 168 func (suite *EpochLookupSuite) TestEpochForViewWithFallback_EpochFallbackTriggered() { 169 epochs := []epochRange{suite.prevEpoch, suite.currEpoch, suite.nextEpoch} 170 suite.WithLock(func() { 171 suite.epochFallbackTriggered = true 172 }) 173 suite.CommitEpochs(epochs...) 174 suite.CreateAndStartEpochLookup() 175 testEpochForViewWithFallback(suite.T(), suite.lookup, suite.state, epochs...) 176 } 177 178 // TestProtocolEvents_EpochFallbackTriggered tests constructing and subsequently querying 179 // EpochLookup, where there is no epoch fallback at construction time, 180 // but an epoch fallback happens later via an epoch event. 181 func (suite *EpochLookupSuite) TestProtocolEvents_EpochFallbackTriggered() { 182 // initially, only current epoch is committed 183 suite.CommitEpochs(suite.currEpoch) 184 suite.CreateAndStartEpochLookup() 185 186 // trigger epoch fallback 187 suite.WithLock(func() { 188 suite.epochFallbackTriggered = true 189 }) 190 suite.lookup.EpochEmergencyFallbackTriggered() 191 192 // wait for the protocol event to be processed (async) 193 assert.Eventually(suite.T(), func() bool { 194 _, err := suite.lookup.EpochForViewWithFallback(suite.currEpoch.finalView + 1) 195 return err == nil 196 }, 5*time.Second, 50*time.Millisecond) 197 198 // validate queries are answered correctly 199 testEpochForViewWithFallback(suite.T(), suite.lookup, suite.state, suite.currEpoch) 200 201 // should handle multiple deliveries of the protocol event 202 suite.lookup.EpochEmergencyFallbackTriggered() 203 suite.lookup.EpochEmergencyFallbackTriggered() 204 suite.lookup.EpochEmergencyFallbackTriggered() 205 206 // validate queries are answered correctly 207 testEpochForViewWithFallback(suite.T(), suite.lookup, suite.state, suite.currEpoch) 208 } 209 210 // TestProtocolEvents_CommittedEpoch tests correct processing of an `EpochCommittedPhaseStarted` event 211 func (suite *EpochLookupSuite) TestProtocolEvents_CommittedEpoch() { 212 // initially, only current epoch is committed 213 suite.CommitEpochs(suite.currEpoch) 214 suite.CreateAndStartEpochLookup() 215 216 // commit the next epoch, and emit a protocol event 217 firstBlockOfCommittedPhase := unittest.BlockHeaderFixture() 218 suite.state.On("AtBlockID", firstBlockOfCommittedPhase.ID()).Return(suite.snapshot) 219 suite.CommitEpochs(suite.nextEpoch) 220 suite.lookup.EpochCommittedPhaseStarted(suite.currentEpochCounter, firstBlockOfCommittedPhase) 221 222 // wait for the protocol event to be processed (async) 223 assert.Eventually(suite.T(), func() bool { 224 _, err := suite.lookup.EpochForViewWithFallback(suite.currEpoch.finalView + 1) 225 return err == nil 226 }, 5*time.Second, 50*time.Millisecond) 227 228 // validate queries are answered correctly 229 testEpochForViewWithFallback(suite.T(), suite.lookup, suite.state, suite.currEpoch, suite.nextEpoch) 230 231 // should handle multiple deliveries of the protocol event 232 suite.lookup.EpochCommittedPhaseStarted(suite.currentEpochCounter, firstBlockOfCommittedPhase) 233 suite.lookup.EpochCommittedPhaseStarted(suite.currentEpochCounter, firstBlockOfCommittedPhase) 234 suite.lookup.EpochCommittedPhaseStarted(suite.currentEpochCounter, firstBlockOfCommittedPhase) 235 236 // validate queries are answered correctly 237 testEpochForViewWithFallback(suite.T(), suite.lookup, suite.state, suite.currEpoch, suite.nextEpoch) 238 } 239 240 // testEpochForViewWithFallback accepts a constructed EpochLookup and state, and 241 // validates correctness by issuing various queries, using the input state and 242 // epochs as source of truth. 243 func testEpochForViewWithFallback(t *testing.T, lookup *EpochLookup, state protocol.State, epochs ...epochRange) { 244 epochFallbackTriggered, err := state.Params().EpochFallbackTriggered() 245 require.NoError(t, err) 246 247 t.Run("should have set epoch fallback triggered correctly", func(t *testing.T) { 248 assert.Equal(t, epochFallbackTriggered, lookup.epochFallbackIsTriggered.Load()) 249 }) 250 251 t.Run("should be able to query within any committed epoch", func(t *testing.T) { 252 for _, epoch := range epochs { 253 t.Run("first view", func(t *testing.T) { 254 counter, err := lookup.EpochForViewWithFallback(epoch.firstView) 255 assert.NoError(t, err) 256 assert.Equal(t, epoch.counter, counter) 257 }) 258 t.Run("final view", func(t *testing.T) { 259 counter, err := lookup.EpochForViewWithFallback(epoch.finalView) 260 assert.NoError(t, err) 261 assert.Equal(t, epoch.counter, counter) 262 }) 263 t.Run("random view in range", func(t *testing.T) { 264 counter, err := lookup.EpochForViewWithFallback(unittest.Uint64InRange(epoch.firstView, epoch.finalView)) 265 assert.NoError(t, err) 266 assert.Equal(t, epoch.counter, counter) 267 }) 268 } 269 }) 270 271 t.Run("should return ErrViewForUnknownEpoch below earliest epoch", func(t *testing.T) { 272 t.Run("view 0", func(t *testing.T) { 273 _, err := lookup.EpochForViewWithFallback(0) 274 assert.ErrorIs(t, err, model.ErrViewForUnknownEpoch) 275 }) 276 t.Run("boundary of earliest epoch", func(t *testing.T) { 277 _, err := lookup.EpochForViewWithFallback(epochs[0].firstView - 1) 278 assert.ErrorIs(t, err, model.ErrViewForUnknownEpoch) 279 }) 280 t.Run("random view below earliest epoch", func(t *testing.T) { 281 _, err := lookup.EpochForViewWithFallback(unittest.Uint64InRange(0, epochs[0].firstView-1)) 282 assert.ErrorIs(t, err, model.ErrViewForUnknownEpoch) 283 }) 284 }) 285 286 // if epoch fallback is triggered, fallback to returning latest epoch counter 287 // otherwise return ErrViewForUnknownEpoch 288 if epochFallbackTriggered { 289 t.Run("should use fallback logic for queries above latest epoch when epoch fallback is triggered", func(t *testing.T) { 290 counter, err := lookup.EpochForViewWithFallback(epochs[len(epochs)-1].finalView + 1) 291 assert.NoError(t, err) 292 // should fallback to returning the counter for the latest epoch 293 assert.Equal(t, epochs[len(epochs)-1].counter, counter) 294 }) 295 } else { 296 t.Run("should return ErrViewForUnknownEpoch for queries above latest epoch when epoch fallback is not triggered", func(t *testing.T) { 297 _, err := lookup.EpochForViewWithFallback(epochs[len(epochs)-1].finalView + 1) 298 assert.ErrorIs(t, err, model.ErrViewForUnknownEpoch) 299 }) 300 } 301 } 302 303 // newMockEpoch returns a mock epoch with the given fields set. 304 func newMockEpoch(counter, firstView, finalView uint64) *mockprotocol.Epoch { 305 epoch := new(mockprotocol.Epoch) 306 epoch.On("FirstView").Return(firstView, nil) 307 epoch.On("FinalView").Return(finalView, nil) 308 epoch.On("Counter").Return(counter, nil) 309 return epoch 310 }