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

     1  package pacemaker
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"math/rand"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/mock"
    12  	"github.com/stretchr/testify/require"
    13  	"github.com/stretchr/testify/suite"
    14  
    15  	"github.com/onflow/flow-go/consensus/hotstuff"
    16  	"github.com/onflow/flow-go/consensus/hotstuff/helper"
    17  	"github.com/onflow/flow-go/consensus/hotstuff/mocks"
    18  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    19  	"github.com/onflow/flow-go/consensus/hotstuff/pacemaker/timeout"
    20  	"github.com/onflow/flow-go/model/flow"
    21  	"github.com/onflow/flow-go/utils/unittest"
    22  )
    23  
    24  const (
    25  	minRepTimeout             float64 = 100.0 // Milliseconds
    26  	maxRepTimeout             float64 = 600.0 // Milliseconds
    27  	multiplicativeIncrease    float64 = 1.5   // multiplicative factor
    28  	happyPathMaxRoundFailures uint64  = 6     // number of failed rounds before first timeout increase
    29  )
    30  
    31  func expectedTimerInfo(view uint64) interface{} {
    32  	return mock.MatchedBy(
    33  		func(timerInfo model.TimerInfo) bool {
    34  			return timerInfo.View == view
    35  		})
    36  }
    37  
    38  func TestActivePaceMaker(t *testing.T) {
    39  	suite.Run(t, new(ActivePaceMakerTestSuite))
    40  }
    41  
    42  type ActivePaceMakerTestSuite struct {
    43  	suite.Suite
    44  
    45  	initialView uint64
    46  	initialQC   *flow.QuorumCertificate
    47  	initialTC   *flow.TimeoutCertificate
    48  
    49  	notifier                 *mocks.Consumer
    50  	proposalDurationProvider hotstuff.ProposalDurationProvider
    51  	persist                  *mocks.Persister
    52  	paceMaker                *ActivePaceMaker
    53  	stop                     context.CancelFunc
    54  	timeoutConf              timeout.Config
    55  }
    56  
    57  func (s *ActivePaceMakerTestSuite) SetupTest() {
    58  	s.initialView = 3
    59  	s.initialQC = QC(2)
    60  	s.initialTC = nil
    61  	var err error
    62  
    63  	s.timeoutConf, err = timeout.NewConfig(time.Duration(minRepTimeout*1e6), time.Duration(maxRepTimeout*1e6), multiplicativeIncrease, happyPathMaxRoundFailures, time.Duration(maxRepTimeout*1e6))
    64  	require.NoError(s.T(), err)
    65  
    66  	// init consumer for notifications emitted by PaceMaker
    67  	s.notifier = mocks.NewConsumer(s.T())
    68  	s.notifier.On("OnStartingTimeout", expectedTimerInfo(s.initialView)).Return().Once()
    69  
    70  	// init Persister dependency for PaceMaker
    71  	// CAUTION: The Persister hands a pointer to `livenessData` to the PaceMaker, which means the PaceMaker
    72  	// could modify our struct in-place. `livenessData` should not be used by tests to determine expected values!
    73  	s.persist = mocks.NewPersister(s.T())
    74  	livenessData := &hotstuff.LivenessData{
    75  		CurrentView: 3,
    76  		LastViewTC:  nil,
    77  		NewestQC:    s.initialQC,
    78  	}
    79  	s.persist.On("GetLivenessData").Return(livenessData, nil)
    80  
    81  	// init PaceMaker and start
    82  	s.paceMaker, err = New(timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist)
    83  	require.NoError(s.T(), err)
    84  
    85  	var ctx context.Context
    86  	ctx, s.stop = context.WithCancel(context.Background())
    87  	s.paceMaker.Start(ctx)
    88  }
    89  
    90  func (s *ActivePaceMakerTestSuite) TearDownTest() {
    91  	s.stop()
    92  }
    93  
    94  func QC(view uint64) *flow.QuorumCertificate {
    95  	return helper.MakeQC(helper.WithQCView(view))
    96  }
    97  
    98  func LivenessData(qc *flow.QuorumCertificate) *hotstuff.LivenessData {
    99  	return &hotstuff.LivenessData{
   100  		CurrentView: qc.View + 1,
   101  		LastViewTC:  nil,
   102  		NewestQC:    qc,
   103  	}
   104  }
   105  
   106  // TestProcessQC_SkipIncreaseViewThroughQC tests that ActivePaceMaker increases view when receiving QC,
   107  // if applicable, by skipping views
   108  func (s *ActivePaceMakerTestSuite) TestProcessQC_SkipIncreaseViewThroughQC() {
   109  	// seeing a QC for the current view should advance the view by one
   110  	qc := QC(s.initialView)
   111  	s.persist.On("PutLivenessData", LivenessData(qc)).Return(nil).Once()
   112  	s.notifier.On("OnStartingTimeout", expectedTimerInfo(4)).Return().Once()
   113  	s.notifier.On("OnQcTriggeredViewChange", s.initialView, uint64(4), qc).Return().Once()
   114  	s.notifier.On("OnViewChange", s.initialView, qc.View+1).Once()
   115  	nve, err := s.paceMaker.ProcessQC(qc)
   116  	require.NoError(s.T(), err)
   117  	require.Equal(s.T(), qc.View+1, s.paceMaker.CurView())
   118  	require.True(s.T(), nve.View == qc.View+1)
   119  	require.Equal(s.T(), qc, s.paceMaker.NewestQC())
   120  	require.Nil(s.T(), s.paceMaker.LastViewTC())
   121  
   122  	// seeing a QC for 10 views in the future should advance to view +11
   123  	curView := s.paceMaker.CurView()
   124  	qc = QC(curView + 10)
   125  	s.persist.On("PutLivenessData", LivenessData(qc)).Return(nil).Once()
   126  	s.notifier.On("OnStartingTimeout", expectedTimerInfo(qc.View+1)).Return().Once()
   127  	s.notifier.On("OnQcTriggeredViewChange", curView, qc.View+1, qc).Return().Once()
   128  	s.notifier.On("OnViewChange", curView, qc.View+1).Once()
   129  	nve, err = s.paceMaker.ProcessQC(qc)
   130  	require.NoError(s.T(), err)
   131  	require.True(s.T(), nve.View == qc.View+1)
   132  	require.Equal(s.T(), qc, s.paceMaker.NewestQC())
   133  	require.Nil(s.T(), s.paceMaker.LastViewTC())
   134  
   135  	require.Equal(s.T(), qc.View+1, s.paceMaker.CurView())
   136  }
   137  
   138  // TestProcessTC_SkipIncreaseViewThroughTC tests that ActivePaceMaker increases view when receiving TC,
   139  // if applicable, by skipping views
   140  func (s *ActivePaceMakerTestSuite) TestProcessTC_SkipIncreaseViewThroughTC() {
   141  	// seeing a TC for the current view should advance the view by one
   142  	tc := helper.MakeTC(helper.WithTCView(s.initialView), helper.WithTCNewestQC(s.initialQC))
   143  	expectedLivenessData := &hotstuff.LivenessData{
   144  		CurrentView: tc.View + 1,
   145  		LastViewTC:  tc,
   146  		NewestQC:    s.initialQC,
   147  	}
   148  	s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once()
   149  	s.notifier.On("OnStartingTimeout", expectedTimerInfo(tc.View+1)).Return().Once()
   150  	s.notifier.On("OnTcTriggeredViewChange", s.initialView, tc.View+1, tc).Return().Once()
   151  	s.notifier.On("OnViewChange", s.initialView, tc.View+1).Once()
   152  	nve, err := s.paceMaker.ProcessTC(tc)
   153  	require.NoError(s.T(), err)
   154  	require.Equal(s.T(), tc.View+1, s.paceMaker.CurView())
   155  	require.True(s.T(), nve.View == tc.View+1)
   156  	require.Equal(s.T(), tc, s.paceMaker.LastViewTC())
   157  
   158  	// seeing a TC for 10 views in the future should advance to view +11
   159  	curView := s.paceMaker.CurView()
   160  	tc = helper.MakeTC(helper.WithTCView(curView+10), helper.WithTCNewestQC(s.initialQC))
   161  	expectedLivenessData = &hotstuff.LivenessData{
   162  		CurrentView: tc.View + 1,
   163  		LastViewTC:  tc,
   164  		NewestQC:    s.initialQC,
   165  	}
   166  	s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once()
   167  	s.notifier.On("OnStartingTimeout", expectedTimerInfo(tc.View+1)).Return().Once()
   168  	s.notifier.On("OnTcTriggeredViewChange", curView, tc.View+1, tc).Return().Once()
   169  	s.notifier.On("OnViewChange", curView, tc.View+1).Once()
   170  	nve, err = s.paceMaker.ProcessTC(tc)
   171  	require.NoError(s.T(), err)
   172  	require.True(s.T(), nve.View == tc.View+1)
   173  	require.Equal(s.T(), tc, s.paceMaker.LastViewTC())
   174  	require.Equal(s.T(), tc.NewestQC, s.paceMaker.NewestQC())
   175  
   176  	require.Equal(s.T(), tc.View+1, s.paceMaker.CurView())
   177  }
   178  
   179  // TestProcessTC_IgnoreOldTC tests that ActivePaceMaker ignores old TC and doesn't advance round.
   180  func (s *ActivePaceMakerTestSuite) TestProcessTC_IgnoreOldTC() {
   181  	nve, err := s.paceMaker.ProcessTC(helper.MakeTC(helper.WithTCView(s.initialView-1),
   182  		helper.WithTCNewestQC(s.initialQC)))
   183  	require.NoError(s.T(), err)
   184  	require.Nil(s.T(), nve)
   185  	require.Equal(s.T(), s.initialView, s.paceMaker.CurView())
   186  }
   187  
   188  // TestProcessTC_IgnoreNilTC tests that ActivePaceMaker accepts nil TC as allowed input but doesn't trigger a new view event
   189  func (s *ActivePaceMakerTestSuite) TestProcessTC_IgnoreNilTC() {
   190  	nve, err := s.paceMaker.ProcessTC(nil)
   191  	require.NoError(s.T(), err)
   192  	require.Nil(s.T(), nve)
   193  	require.Equal(s.T(), s.initialView, s.paceMaker.CurView())
   194  }
   195  
   196  // TestProcessQC_PersistException tests that ActivePaceMaker propagates exception
   197  // when processing QC
   198  func (s *ActivePaceMakerTestSuite) TestProcessQC_PersistException() {
   199  	exception := errors.New("persist-exception")
   200  	qc := QC(s.initialView)
   201  	s.persist.On("PutLivenessData", mock.Anything).Return(exception).Once()
   202  	nve, err := s.paceMaker.ProcessQC(qc)
   203  	require.Nil(s.T(), nve)
   204  	require.ErrorIs(s.T(), err, exception)
   205  }
   206  
   207  // TestProcessTC_PersistException tests that ActivePaceMaker propagates exception
   208  // when processing TC
   209  func (s *ActivePaceMakerTestSuite) TestProcessTC_PersistException() {
   210  	exception := errors.New("persist-exception")
   211  	tc := helper.MakeTC(helper.WithTCView(s.initialView))
   212  	s.persist.On("PutLivenessData", mock.Anything).Return(exception).Once()
   213  	nve, err := s.paceMaker.ProcessTC(tc)
   214  	require.Nil(s.T(), nve)
   215  	require.ErrorIs(s.T(), err, exception)
   216  }
   217  
   218  // TestProcessQC_InvalidatesLastViewTC verifies that PaceMaker does not retain any old
   219  // TC if the last view change was triggered by observing a QC from the previous view.
   220  func (s *ActivePaceMakerTestSuite) TestProcessQC_InvalidatesLastViewTC() {
   221  	tc := helper.MakeTC(helper.WithTCView(s.initialView+1), helper.WithTCNewestQC(s.initialQC))
   222  	s.persist.On("PutLivenessData", mock.Anything).Return(nil).Times(2)
   223  	s.notifier.On("OnStartingTimeout", mock.Anything).Return().Times(2)
   224  	s.notifier.On("OnTcTriggeredViewChange", mock.Anything, mock.Anything, mock.Anything).Return().Once()
   225  	s.notifier.On("OnQcTriggeredViewChange", mock.Anything, mock.Anything, mock.Anything).Return().Once()
   226  	s.notifier.On("OnViewChange", s.initialView, tc.View+1).Once()
   227  	nve, err := s.paceMaker.ProcessTC(tc)
   228  	require.NotNil(s.T(), nve)
   229  	require.NoError(s.T(), err)
   230  	require.NotNil(s.T(), s.paceMaker.LastViewTC())
   231  
   232  	qc := QC(tc.View + 1)
   233  	s.notifier.On("OnViewChange", tc.View+1, qc.View+1).Once()
   234  	nve, err = s.paceMaker.ProcessQC(qc)
   235  	require.NotNil(s.T(), nve)
   236  	require.NoError(s.T(), err)
   237  	require.Nil(s.T(), s.paceMaker.LastViewTC())
   238  }
   239  
   240  // TestProcessQC_IgnoreOldQC tests that ActivePaceMaker ignores old QC and doesn't advance round
   241  func (s *ActivePaceMakerTestSuite) TestProcessQC_IgnoreOldQC() {
   242  	qc := QC(s.initialView - 1)
   243  	nve, err := s.paceMaker.ProcessQC(qc)
   244  	require.NoError(s.T(), err)
   245  	require.Nil(s.T(), nve)
   246  	require.Equal(s.T(), s.initialView, s.paceMaker.CurView())
   247  	require.NotEqual(s.T(), qc, s.paceMaker.NewestQC())
   248  }
   249  
   250  // TestProcessQC_UpdateNewestQC tests that ActivePaceMaker tracks the newest QC even if it has advanced past this view.
   251  // In this test, we feed a newer QC as part of a TC into the PaceMaker.
   252  func (s *ActivePaceMakerTestSuite) TestProcessQC_UpdateNewestQC() {
   253  	tc := helper.MakeTC(helper.WithTCView(s.initialView+10), helper.WithTCNewestQC(s.initialQC))
   254  	expectedView := tc.View + 1
   255  	s.notifier.On("OnTcTriggeredViewChange", mock.Anything, mock.Anything, mock.Anything).Return().Once()
   256  	s.notifier.On("OnViewChange", s.initialView, expectedView).Once()
   257  	s.notifier.On("OnStartingTimeout", mock.Anything).Return().Once()
   258  	s.persist.On("PutLivenessData", mock.Anything).Return(nil).Once()
   259  	nve, err := s.paceMaker.ProcessTC(tc)
   260  	require.NoError(s.T(), err)
   261  	require.NotNil(s.T(), nve)
   262  
   263  	qc := QC(s.initialView + 5)
   264  	expectedLivenessData := &hotstuff.LivenessData{
   265  		CurrentView: expectedView,
   266  		LastViewTC:  tc,
   267  		NewestQC:    qc,
   268  	}
   269  	s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once()
   270  
   271  	nve, err = s.paceMaker.ProcessQC(qc)
   272  	require.NoError(s.T(), err)
   273  	require.Nil(s.T(), nve)
   274  	require.Equal(s.T(), qc, s.paceMaker.NewestQC())
   275  }
   276  
   277  // TestProcessTC_UpdateNewestQC tests that ActivePaceMaker tracks the newest QC included in TC even if it has advanced past this view.
   278  func (s *ActivePaceMakerTestSuite) TestProcessTC_UpdateNewestQC() {
   279  	tc := helper.MakeTC(helper.WithTCView(s.initialView+10), helper.WithTCNewestQC(s.initialQC))
   280  	expectedView := tc.View + 1
   281  	s.notifier.On("OnTcTriggeredViewChange", mock.Anything, mock.Anything, mock.Anything).Return().Once()
   282  	s.notifier.On("OnViewChange", s.initialView, expectedView).Once()
   283  	s.notifier.On("OnStartingTimeout", mock.Anything).Return().Once()
   284  	s.persist.On("PutLivenessData", mock.Anything).Return(nil).Once()
   285  	nve, err := s.paceMaker.ProcessTC(tc)
   286  	require.NoError(s.T(), err)
   287  	require.NotNil(s.T(), nve)
   288  
   289  	qc := QC(s.initialView + 5)
   290  	olderTC := helper.MakeTC(helper.WithTCView(s.paceMaker.CurView()-1), helper.WithTCNewestQC(qc))
   291  	expectedLivenessData := &hotstuff.LivenessData{
   292  		CurrentView: expectedView,
   293  		LastViewTC:  tc,
   294  		NewestQC:    qc,
   295  	}
   296  	s.persist.On("PutLivenessData", expectedLivenessData).Return(nil).Once()
   297  
   298  	nve, err = s.paceMaker.ProcessTC(olderTC)
   299  	require.NoError(s.T(), err)
   300  	require.Nil(s.T(), nve)
   301  	require.Equal(s.T(), qc, s.paceMaker.NewestQC())
   302  }
   303  
   304  // Test_Initialization tests QCs and TCs provided as optional constructor arguments.
   305  // We want to test that nil, old and duplicate TCs & QCs are accepted in arbitrary order.
   306  // The constructed PaceMaker should be in the state:
   307  //   - in view V+1, where V is the _largest view of _any_ of the ingested QCs and TCs
   308  //   - method `NewestQC` should report the QC with the highest View in _any_ of the inputs
   309  func (s *ActivePaceMakerTestSuite) Test_Initialization() {
   310  	highestView := uint64(0) // highest View of any QC or TC constructed below
   311  
   312  	// Randomly create 80 TCs:
   313  	//  * their view is randomly sampled from the range [3, 103)
   314  	//  * as we sample 80 times, probability of creating 2 TCs for the same
   315  	//    view is practically 1 (-> birthday problem)
   316  	//  * we place the TCs in a slice of length 110, i.e. some elements are guaranteed to be nil
   317  	//  * Note: we specifically allow for the TC to have the same view as the highest QC.
   318  	//    This is useful as a fallback, because it allows replicas other than the designated
   319  	//     leader to also collect votes and generate a QC.
   320  	tcs := make([]*flow.TimeoutCertificate, 110)
   321  	for i := 0; i < 80; i++ {
   322  		tcView := s.initialView + uint64(rand.Intn(100))
   323  		qcView := 1 + uint64(rand.Intn(int(tcView)))
   324  		tcs[i] = helper.MakeTC(helper.WithTCView(tcView), helper.WithTCNewestQC(QC(qcView)))
   325  		highestView = max(highestView, tcView, qcView)
   326  	}
   327  	rand.Shuffle(len(tcs), func(i, j int) {
   328  		tcs[i], tcs[j] = tcs[j], tcs[i]
   329  	})
   330  
   331  	// randomly create 80 QCs (same logic as above)
   332  	qcs := make([]*flow.QuorumCertificate, 110)
   333  	for i := 0; i < 80; i++ {
   334  		qcs[i] = QC(s.initialView + uint64(rand.Intn(100)))
   335  		highestView = max(highestView, qcs[i].View)
   336  	}
   337  	rand.Shuffle(len(qcs), func(i, j int) {
   338  		qcs[i], qcs[j] = qcs[j], qcs[i]
   339  	})
   340  
   341  	// set up mocks
   342  	s.persist.On("PutLivenessData", mock.Anything).Return(nil)
   343  
   344  	// test that the constructor finds the newest QC and TC
   345  	s.Run("Random TCs and QCs combined", func() {
   346  		pm, err := New(
   347  			timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist,
   348  			WithQCs(qcs...), WithTCs(tcs...),
   349  		)
   350  		require.NoError(s.T(), err)
   351  
   352  		require.Equal(s.T(), highestView+1, pm.CurView())
   353  		if tc := pm.LastViewTC(); tc != nil {
   354  			require.Equal(s.T(), highestView, tc.View)
   355  		} else {
   356  			require.Equal(s.T(), highestView, pm.NewestQC().View)
   357  		}
   358  	})
   359  
   360  	// We specifically test an edge case: an outdated TC can still contain a QC that
   361  	// is newer than the newest QC the pacemaker knows so far.
   362  	s.Run("Newest QC in older TC", func() {
   363  		tcs[17] = helper.MakeTC(helper.WithTCView(highestView+20), helper.WithTCNewestQC(QC(highestView+5)))
   364  		tcs[45] = helper.MakeTC(helper.WithTCView(highestView+15), helper.WithTCNewestQC(QC(highestView+12)))
   365  
   366  		pm, err := New(
   367  			timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist,
   368  			WithTCs(tcs...), WithQCs(qcs...),
   369  		)
   370  		require.NoError(s.T(), err)
   371  
   372  		// * when observing tcs[17], which is newer than any other QC or TC, the pacemaker should enter view tcs[17].View + 1
   373  		// * when observing tcs[45], which is older than tcs[17], the PaceMaker should notice that the QC in tcs[45]
   374  		//   is newer than its local QC and update it
   375  		require.Equal(s.T(), tcs[17].View+1, pm.CurView())
   376  		require.Equal(s.T(), tcs[17], pm.LastViewTC())
   377  		require.Equal(s.T(), tcs[45].NewestQC, pm.NewestQC())
   378  	})
   379  
   380  	// Another edge case: a TC from a past view contains QC for the same view.
   381  	// While is TC is outdated, the contained QC is still newer that the QC the pacemaker knows so far.
   382  	s.Run("Newest QC in older TC", func() {
   383  		tcs[17] = helper.MakeTC(helper.WithTCView(highestView+20), helper.WithTCNewestQC(QC(highestView+5)))
   384  		tcs[45] = helper.MakeTC(helper.WithTCView(highestView+15), helper.WithTCNewestQC(QC(highestView+15)))
   385  
   386  		pm, err := New(
   387  			timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist,
   388  			WithTCs(tcs...), WithQCs(qcs...),
   389  		)
   390  		require.NoError(s.T(), err)
   391  
   392  		// * when observing tcs[17], which is newer than any other QC or TC, the pacemaker should enter view tcs[17].View + 1
   393  		// * when observing tcs[45], which is older than tcs[17], the PaceMaker should notice that the QC in tcs[45]
   394  		//   is newer than its local QC and update it
   395  		require.Equal(s.T(), tcs[17].View+1, pm.CurView())
   396  		require.Equal(s.T(), tcs[17], pm.LastViewTC())
   397  		require.Equal(s.T(), tcs[45].NewestQC, pm.NewestQC())
   398  	})
   399  
   400  	// Verify that WithTCs still works correctly if no TCs are given:
   401  	// the list of TCs is empty or all contained TCs are nil
   402  	s.Run("Only nil TCs", func() {
   403  		pm, err := New(timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, WithTCs())
   404  		require.NoError(s.T(), err)
   405  		require.Equal(s.T(), s.initialView, pm.CurView())
   406  
   407  		pm, err = New(timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, WithTCs(nil, nil, nil))
   408  		require.NoError(s.T(), err)
   409  		require.Equal(s.T(), s.initialView, pm.CurView())
   410  	})
   411  
   412  	// Verify that WithQCs still works correctly if no QCs are given:
   413  	// the list of QCs is empty or all contained QCs are nil
   414  	s.Run("Only nil QCs", func() {
   415  		pm, err := New(timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, WithQCs())
   416  		require.NoError(s.T(), err)
   417  		require.Equal(s.T(), s.initialView, pm.CurView())
   418  
   419  		pm, err = New(timeout.NewController(s.timeoutConf), NoProposalDelay(), s.notifier, s.persist, WithQCs(nil, nil, nil))
   420  		require.NoError(s.T(), err)
   421  		require.Equal(s.T(), s.initialView, pm.CurView())
   422  	})
   423  
   424  }
   425  
   426  // TestProposalDuration tests that the active pacemaker forwards proposal duration values from the provider.
   427  func (s *ActivePaceMakerTestSuite) TestProposalDuration() {
   428  	proposalDurationProvider := NewStaticProposalDurationProvider(time.Millisecond * 500)
   429  	pm, err := New(timeout.NewController(s.timeoutConf), &proposalDurationProvider, s.notifier, s.persist)
   430  	require.NoError(s.T(), err)
   431  
   432  	now := time.Now().UTC()
   433  	assert.Equal(s.T(), now.Add(time.Millisecond*500), pm.TargetPublicationTime(117, now, unittest.IdentifierFixture()))
   434  	proposalDurationProvider.dur = time.Second
   435  	assert.Equal(s.T(), now.Add(time.Second), pm.TargetPublicationTime(117, now, unittest.IdentifierFixture()))
   436  }
   437  
   438  func max(a uint64, values ...uint64) uint64 {
   439  	for _, v := range values {
   440  		if v > a {
   441  			a = v
   442  		}
   443  	}
   444  	return a
   445  }