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  }