diff --git a/common/countdown/countdown.go b/common/countdown/countdown.go index cffba13bf080..b6d7462115ad 100644 --- a/common/countdown/countdown.go +++ b/common/countdown/countdown.go @@ -5,26 +5,40 @@ import ( "sync" "time" + "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/log" ) +type TimeoutDurationHelper interface { + GetTimeoutDuration(types.Round, types.Round) time.Duration + SetParams(time.Duration, float64, uint8) error +} + type CountdownTimer struct { - lock sync.RWMutex // Protects the Initilised field - resetc chan int - quitc chan chan struct{} - initilised bool - timeoutDuration time.Duration + lock sync.RWMutex // Protects the Initilised field + resetc chan ResetInfo + quitc chan chan struct{} + initilised bool + durationHelper TimeoutDurationHelper // Triggered when the countdown timer timeout for the `timeoutDuration` period, it will pass current timestamp to the callback function OnTimeoutFn func(time time.Time, i interface{}) error } -func NewCountDown(duration time.Duration) *CountdownTimer { - return &CountdownTimer{ - resetc: make(chan int), - quitc: make(chan chan struct{}), - initilised: false, - timeoutDuration: duration, +type ResetInfo struct { + currentRound, highestRound types.Round +} + +func NewExpCountDown(duration time.Duration, base float64, max_exponent uint8) (*CountdownTimer, error) { + durationHelper, err := NewExpTimeoutDuration(duration, base, max_exponent) + if err != nil { + return nil, err } + return &CountdownTimer{ + resetc: make(chan ResetInfo), + quitc: make(chan chan struct{}), + initilised: false, + durationHelper: durationHelper, + }, nil } // Completely stop the countdown timer from running. @@ -34,25 +48,25 @@ func (t *CountdownTimer) StopTimer() { <-q } -func (t *CountdownTimer) SetTimeoutDuration(duration time.Duration) { - t.timeoutDuration = duration +func (t *CountdownTimer) SetParams(duration time.Duration, base float64, maxExponent uint8) error { + return t.durationHelper.SetParams(duration, base, maxExponent) } // Reset will start the countdown timer if it's already stopped, or simply reset the countdown time back to the defual `duration` -func (t *CountdownTimer) Reset(i interface{}) { +func (t *CountdownTimer) Reset(i interface{}, currentRound, highestRound types.Round) { if !t.isInitilised() { t.setInitilised(true) - go t.startTimer(i) + go t.startTimer(i, currentRound, highestRound) } else { - t.resetc <- 0 + t.resetc <- ResetInfo{currentRound, highestRound} } } // A long running process that -func (t *CountdownTimer) startTimer(i interface{}) { +func (t *CountdownTimer) startTimer(i interface{}, currentRound, highestRound types.Round) { // Make sure we mark Initilised to false when we quit the countdown defer t.setInitilised(false) - timer := time.NewTimer(t.timeoutDuration) + timer := time.NewTimer(t.durationHelper.GetTimeoutDuration(currentRound, highestRound)) // We start with a inf loop for { select { @@ -69,10 +83,15 @@ func (t *CountdownTimer) startTimer(i interface{}) { } log.Debug("OnTimeoutFn processed") }() - timer.Reset(t.timeoutDuration) - case <-t.resetc: + timer.Reset(t.durationHelper.GetTimeoutDuration(currentRound, highestRound)) + case info := <-t.resetc: log.Debug("Reset countdown timer") - timer.Reset(t.timeoutDuration) + currentRound = info.currentRound + highestRound = info.highestRound + if !timer.Stop() { + <-timer.C + } + timer.Reset(t.durationHelper.GetTimeoutDuration(currentRound, highestRound)) } } } diff --git a/common/countdown/countdown_test.go b/common/countdown/countdown_test.go index 6f1b0e10225e..e10072531ac8 100644 --- a/common/countdown/countdown_test.go +++ b/common/countdown/countdown_test.go @@ -16,9 +16,10 @@ func TestCountdownWillCallback(t *testing.T) { return nil } - countdown := NewCountDown(1000 * time.Millisecond) + countdown, err := NewExpCountDown(1000*time.Millisecond, 0, 0) + assert.Nil(t, err) countdown.OnTimeoutFn = OnTimeoutFn - countdown.Reset(fakeI) + countdown.Reset(fakeI, 0, 0) <-called t.Log("Times up, successfully called OnTimeoutFn") } @@ -31,11 +32,12 @@ func TestCountdownShouldReset(t *testing.T) { return nil } - countdown := NewCountDown(5000 * time.Millisecond) + countdown, err := NewExpCountDown(5000*time.Millisecond, 0, 0) + assert.Nil(t, err) countdown.OnTimeoutFn = OnTimeoutFn // Check countdown did not start assert.False(t, countdown.isInitilised()) - countdown.Reset(fakeI) + countdown.Reset(fakeI, 0, 0) // Now the countdown should already started assert.True(t, countdown.isInitilised()) expectedCalledTime := time.Now().Add(9000 * time.Millisecond) @@ -54,7 +56,7 @@ firstReset: } break firstReset case <-resetTimer.C: - countdown.Reset(fakeI) + countdown.Reset(fakeI, 0, 0) } } @@ -79,11 +81,12 @@ func TestCountdownShouldResetEvenIfErrored(t *testing.T) { return errors.New("ERROR!") } - countdown := NewCountDown(5000 * time.Millisecond) + countdown, err := NewExpCountDown(5000*time.Millisecond, 0, 0) + assert.Nil(t, err) countdown.OnTimeoutFn = OnTimeoutFn // Check countdown did not start assert.False(t, countdown.isInitilised()) - countdown.Reset(fakeI) + countdown.Reset(fakeI, 0, 0) // Now the countdown should already started assert.True(t, countdown.isInitilised()) expectedCalledTime := time.Now().Add(9000 * time.Millisecond) @@ -102,7 +105,7 @@ firstReset: } break firstReset case <-resetTimer.C: - countdown.Reset(fakeI) + countdown.Reset(fakeI, 0, 0) } } @@ -127,11 +130,12 @@ func TestCountdownShouldBeAbleToStop(t *testing.T) { return nil } - countdown := NewCountDown(5000 * time.Millisecond) + countdown, err := NewExpCountDown(5000*time.Millisecond, 0, 0) + assert.Nil(t, err) countdown.OnTimeoutFn = OnTimeoutFn // Check countdown did not start assert.False(t, countdown.isInitilised()) - countdown.Reset(fakeI) + countdown.Reset(fakeI, 0, 0) // Now the countdown should already started assert.True(t, countdown.isInitilised()) // Try manually stop the timer before it triggers the callback @@ -144,14 +148,15 @@ func TestCountdownShouldBeAbleToStop(t *testing.T) { func TestCountdownShouldAvoidDeadlock(t *testing.T) { var fakeI interface{} called := make(chan int) - countdown := NewCountDown(5000 * time.Millisecond) + countdown, err := NewExpCountDown(5000*time.Millisecond, 0, 0) + assert.Nil(t, err) OnTimeoutFn := func(time.Time, interface{}) error { - countdown.Reset(fakeI) + countdown.Reset(fakeI, 0, 0) called <- 1 return nil } countdown.OnTimeoutFn = OnTimeoutFn - countdown.Reset(fakeI) + countdown.Reset(fakeI, 0, 0) <-called } diff --git a/common/countdown/exp_duration.go b/common/countdown/exp_duration.go new file mode 100644 index 000000000000..10689bf6307f --- /dev/null +++ b/common/countdown/exp_duration.go @@ -0,0 +1,70 @@ +// A countdown timer that will mostly be used by XDPoS v2 consensus engine +package countdown + +import ( + "fmt" + "math" + "time" + + "github.com/XinFinOrg/XDPoSChain/core/types" +) + +const maxExponentUpperbound uint8 = 32 + +type ExpTimeoutDuration struct { + duration time.Duration + base float64 + maxExponent uint8 +} + +func NewExpTimeoutDuration(duration time.Duration, base float64, maxExponent uint8) (*ExpTimeoutDuration, error) { + d := &ExpTimeoutDuration{ + duration: duration, + base: base, + maxExponent: maxExponent, + } + err := d.sanityCheck() + return d, err +} + +func (d *ExpTimeoutDuration) sanityCheck() error { + if d.maxExponent >= maxExponentUpperbound { + return fmt.Errorf("max_exponent (%d)= >= max_exponent_upperbound (%d)", d.maxExponent, maxExponentUpperbound) + } + if math.Pow(d.base, float64(d.maxExponent)) >= float64(math.MaxUint32) { + return fmt.Errorf("base^max_exponent (%f^%d) should be less than 2^32", d.base, d.maxExponent) + } + return nil +} + +// The inputs should be: currentRound, highestQuorumCert's round +func (d *ExpTimeoutDuration) GetTimeoutDuration(currentRound, highestRound types.Round) time.Duration { + power := float64(1) + // below statement must be true, just to prevent negative result + if highestRound < currentRound { + exp := uint8(currentRound-highestRound) - 1 + if exp > d.maxExponent { + exp = d.maxExponent + } + power = math.Pow(d.base, float64(exp)) + } + return d.duration * time.Duration(power) +} + +func (d *ExpTimeoutDuration) SetParams(duration time.Duration, base float64, maxExponent uint8) error { + prevDuration := d.duration + prevBase := d.base + prevME := d.maxExponent + + d.duration = duration + d.base = base + d.maxExponent = maxExponent + // if parameters are wrong, should remain instead of change or panic + if err := d.sanityCheck(); err != nil { + d.duration = prevDuration + d.base = prevBase + d.maxExponent = prevME + return err + } + return nil +} diff --git a/common/countdown/exp_duration_test.go b/common/countdown/exp_duration_test.go new file mode 100644 index 000000000000..cc7090128376 --- /dev/null +++ b/common/countdown/exp_duration_test.go @@ -0,0 +1,65 @@ +package countdown + +import ( + "math" + "testing" + "time" + + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/stretchr/testify/assert" +) + +func TestExpDuration(t *testing.T) { + base := float64(2.0) + max_exponent := uint8(2) + duration := time.Second * 59 + helper, err := NewExpTimeoutDuration(duration, base, max_exponent) + assert.Nil(t, err) + // round 10 = 9+1, normal case, should be base + currentRound := types.Round(10) + highestQCRound := types.Round(9) + result := helper.GetTimeoutDuration(currentRound, highestQCRound) + assert.Equal(t, duration, result) + + // round 11 = 9+2, already 1 round timeout, should be base*exponent + currentRound++ + result = helper.GetTimeoutDuration(currentRound, highestQCRound) + assert.Equal(t, duration*time.Duration(base), result) + + // round 12 = 9+3, already 2 rounds timeout, should be base*exponent^2 + currentRound++ + result = helper.GetTimeoutDuration(currentRound, highestQCRound) + assert.Equal(t, duration*time.Duration(base)*time.Duration(base), result) + + // test SetParams + duration++ + max_exponent++ + base++ + helper.SetParams(duration, base, max_exponent) + result = helper.GetTimeoutDuration(currentRound, highestQCRound) + assert.Equal(t, duration*time.Duration(base)*time.Duration(base), result) + // round 14 = 9+5, already 4 rounds timeout, but max_exponent=3, should be base*exponent^3 + currentRound++ + currentRound++ + result = helper.GetTimeoutDuration(currentRound, highestQCRound) + assert.Equal(t, duration*time.Duration(math.Pow(base, float64(3))), result) + + // extreme case + helper.SetParams(duration, float64(0), uint8(0)) + result = helper.GetTimeoutDuration(currentRound, highestQCRound) + assert.Equal(t, duration, result) +} + +func TestInvalidParameter(t *testing.T) { + base := float64(2.0) + max_exponent := uint8(32) + duration := time.Second * 59 + _, err := NewExpTimeoutDuration(duration, base, max_exponent) + assert.Error(t, err) + + base = float64(3.0) + max_exponent = uint8(21) + duration = time.Second * 59 + _, err = NewExpTimeoutDuration(duration, base, max_exponent) + assert.Error(t, err) +} diff --git a/consensus/XDPoS/engines/engine_v2/engine.go b/consensus/XDPoS/engines/engine_v2/engine.go index 298b18114388..21830c7a780e 100644 --- a/consensus/XDPoS/engines/engine_v2/engine.go +++ b/consensus/XDPoS/engines/engine_v2/engine.go @@ -76,7 +76,10 @@ func New(chainConfig *params.ChainConfig, db ethdb.Database, minePeriodCh chan i config := chainConfig.XDPoS // Setup timeoutTimer duration := time.Duration(config.V2.CurrentConfig.TimeoutPeriod) * time.Second - timeoutTimer := countdown.NewCountDown(duration) + timeoutTimer, err := countdown.NewExpCountDown(duration, config.V2.CurrentConfig.ExpTimeoutConfig.Base, config.V2.CurrentConfig.ExpTimeoutConfig.MaxExponent) + if err != nil { + log.Crit("create exp countdown", "err", err) + } timeoutPool := utils.NewPool() votePool := utils.NewPool() @@ -139,8 +142,10 @@ func (x *XDPoS_v2) UpdateParams(header *types.Header) { // Setup timeoutTimer duration := time.Duration(x.config.V2.CurrentConfig.TimeoutPeriod) * time.Second - x.timeoutWorker.SetTimeoutDuration(duration) - + err = x.timeoutWorker.SetParams(duration, x.config.V2.CurrentConfig.ExpTimeoutConfig.Base, x.config.V2.CurrentConfig.ExpTimeoutConfig.MaxExponent) + if err != nil { + log.Error("[UpdateParams] set params failed", "err", err) + } // avoid deadlock go func() { x.minePeriodCh <- x.config.V2.CurrentConfig.MinePeriod @@ -253,7 +258,7 @@ func (x *XDPoS_v2) initial(chain consensus.ChainReader, header *types.Header) er }() // Kick-off the countdown timer - x.timeoutWorker.Reset(chain) + x.timeoutWorker.Reset(chain, 0, 0) x.isInitilised = true log.Warn("[initial] finish initialisation") @@ -915,7 +920,7 @@ func (x *XDPoS_v2) setNewRound(blockChainReader consensus.ChainReader, round typ log.Info("[setNewRound] new round and reset pools and workers", "round", round) x.currentRound = round x.timeoutCount = 0 - x.timeoutWorker.Reset(blockChainReader) + x.timeoutWorker.Reset(blockChainReader, x.currentRound, x.highestQuorumCert.ProposedBlockInfo.Round) x.timeoutPool.Clear() // don't need to clean vote pool, we have other process to clean and it's not good to clean here, some edge case may break // for example round gets bump during collecting vote, so we have to keep vote. diff --git a/consensus/XDPoS/engines/engine_v2/testing_utils.go b/consensus/XDPoS/engines/engine_v2/testing_utils.go index 030080b62aa7..92c4dd7119bf 100644 --- a/consensus/XDPoS/engines/engine_v2/testing_utils.go +++ b/consensus/XDPoS/engines/engine_v2/testing_utils.go @@ -15,7 +15,7 @@ func (x *XDPoS_v2) SetNewRoundFaker(blockChainReader consensus.ChainReader, newR defer x.lock.Unlock() // Reset a bunch of things if resetTimer { - x.timeoutWorker.Reset(blockChainReader) + x.timeoutWorker.Reset(blockChainReader, 0, 0) } x.currentRound = newRound } diff --git a/params/config.go b/params/config.go index cb67859d9076..f6d795560d67 100644 --- a/params/config.go +++ b/params/config.go @@ -47,6 +47,7 @@ var ( TimeoutSyncThreshold: 3, TimeoutPeriod: 30, MinePeriod: 2, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, 2000: { MaxMasternodes: 108, @@ -55,6 +56,7 @@ var ( TimeoutSyncThreshold: 2, TimeoutPeriod: 600, MinePeriod: 2, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, 8000: { MaxMasternodes: 108, @@ -63,6 +65,7 @@ var ( TimeoutSyncThreshold: 2, TimeoutPeriod: 60, MinePeriod: 2, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, 220000: { MaxMasternodes: 108, @@ -71,6 +74,7 @@ var ( TimeoutSyncThreshold: 2, TimeoutPeriod: 30, MinePeriod: 2, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, 460000: { MaxMasternodes: 108, @@ -79,6 +83,7 @@ var ( TimeoutSyncThreshold: 2, TimeoutPeriod: 20, MinePeriod: 2, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, } @@ -90,6 +95,7 @@ var ( TimeoutSyncThreshold: 3, TimeoutPeriod: 60, MinePeriod: 2, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, 900000: { MaxMasternodes: 108, @@ -98,6 +104,7 @@ var ( TimeoutSyncThreshold: 3, TimeoutPeriod: 60, MinePeriod: 2, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, } @@ -109,6 +116,7 @@ var ( TimeoutSyncThreshold: 3, TimeoutPeriod: 30, MinePeriod: 2, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, } @@ -120,6 +128,7 @@ var ( TimeoutSyncThreshold: 2, TimeoutPeriod: 4, MinePeriod: 2, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, 10: { MaxMasternodes: 18, @@ -128,6 +137,7 @@ var ( TimeoutSyncThreshold: 2, TimeoutPeriod: 4, MinePeriod: 3, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, 900: { MaxMasternodes: 20, @@ -136,6 +146,7 @@ var ( TimeoutSyncThreshold: 4, TimeoutPeriod: 5, MinePeriod: 2, + ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0}, }, } @@ -448,6 +459,13 @@ type V2Config struct { TimeoutSyncThreshold int `json:"timeoutSyncThreshold"` // send syncInfo after number of timeout TimeoutPeriod int `json:"timeoutPeriod"` // Duration in ms CertThreshold float64 `json:"certificateThreshold"` // Necessary number of messages from master nodes to form a certificate + + ExpTimeoutConfig ExpTimeoutConfig `json:"expTimeoutConfig"` +} + +type ExpTimeoutConfig struct { + Base float64 `json:"base"` // base in base^exponent + MaxExponent uint8 `json:"maxExponent"` // max exponent in base^exponent } func (c *XDPoSConfig) String() string {