Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core/consensus: change exponential timer for linear timer #3470

Merged
merged 3 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/featureset/featureset.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ const (
// unless the user disabled this feature explicitly.
GnosisBlockHotfix Feature = "gnosis_block_hotfix"

// Exponential enables Exponential round timer for consensus rounds.
// Linear enables Linear round timer for consensus rounds.
// When active has precedence over EagerDoubleLinear round timer.
Exponential Feature = "exponential"
Linear Feature = "linear"
)

var (
Expand All @@ -60,7 +60,7 @@ var (
AggSigDBV2: statusAlpha,
JSONRequests: statusAlpha,
GnosisBlockHotfix: statusAlpha,
Exponential: statusAlpha,
Linear: statusAlpha,
// Add all features and there status here.
}

Expand Down
2 changes: 1 addition & 1 deletion app/featureset/featureset_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestAllFeatureStatus(t *testing.T) {
ConsensusParticipate,
JSONRequests,
GnosisBlockHotfix,
Exponential,
Linear,
}

for _, feature := range features {
Expand Down
42 changes: 24 additions & 18 deletions core/consensus/utils/roundtimer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
package utils

import (
"math"
"strings"
"sync"
"time"
Expand All @@ -25,9 +24,16 @@ type TimerFunc func(core.Duty) RoundTimer

// GetTimerFunc returns a timer function based on the enabled features.
func GetTimerFunc() TimerFunc {
if featureset.Enabled(featureset.Exponential) {
return func(core.Duty) RoundTimer {
return NewExponentialRoundTimer()
if featureset.Enabled(featureset.Linear) {
return func(duty core.Duty) RoundTimer {
// Linear timer only affects Proposer duty
if duty.Type == core.DutyProposer {
return NewLinearRoundTimer()
} else if featureset.Enabled(featureset.EagerDoubleLinear) {
return NewDoubleEagerLinearRoundTimer()
}

return NewIncreasingRoundTimer()
}
}

Expand All @@ -54,7 +60,7 @@ func (t TimerType) Eager() bool {
const (
TimerIncreasing TimerType = "inc"
TimerEagerDoubleLinear TimerType = "eager_dlinear"
TimerExponential TimerType = "exponential"
TimerLinear TimerType = "linear"
)

// increasingRoundTimeout returns the duration for a round that starts at incRoundStart in round 1
Expand Down Expand Up @@ -159,40 +165,40 @@ func (t *doubleEagerLinearRoundTimer) Timer(round int64) (<-chan time.Time, func
return timer.Chan(), func() { timer.Stop() }
}

// exponentialRoundTimer implements a round timerType with the following properties:
// linearRoundTimer implements a round timerType with the following properties:
//
// The first round has one second to complete consensus
// If this round fails then other peers already had time to fetch proposal and therefore
// won't need as much time to reach a consensus. Therefore start timeout with lower value
// which will increase exponentially
type exponentialRoundTimer struct {
// which will increase linearly
type linearRoundTimer struct {
clock clockwork.Clock
}

func (*exponentialRoundTimer) Type() TimerType {
return TimerExponential
func (*linearRoundTimer) Type() TimerType {
return TimerLinear
}

func (t *exponentialRoundTimer) Timer(round int64) (<-chan time.Time, func()) {
func (t *linearRoundTimer) Timer(round int64) (<-chan time.Time, func()) {
var timer clockwork.Timer
if round == 1 {
// First round has 1 second
timer = t.clock.NewTimer(time.Second)
} else {
// Subsequent rounds have exponentially more time starting at 200 milliseconds
timer = t.clock.NewTimer(time.Millisecond * time.Duration(math.Pow(2, float64(round-1))*100))
// Subsequent rounds have linearly more time starting at 400 milliseconds
timer = t.clock.NewTimer(time.Duration(200*(round-1) + 200))
}

return timer.Chan(), func() { timer.Stop() }
}

// NewExponentialRoundTimer returns a new exponential round timer type.
func NewExponentialRoundTimer() RoundTimer {
return NewExponentialRoundTimerWithClock(clockwork.NewRealClock())
// NewLinearRoundTimer returns a new linear round timer type.
func NewLinearRoundTimer() RoundTimer {
return NewLinearRoundTimerWithClock(clockwork.NewRealClock())
}

func NewExponentialRoundTimerWithClock(clock clockwork.Clock) RoundTimer {
return &exponentialRoundTimer{
func NewLinearRoundTimerWithClock(clock clockwork.Clock) RoundTimer {
return &linearRoundTimer{
clock: clock,
}
}
29 changes: 20 additions & 9 deletions core/consensus/utils/roundtimer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func TestDoubleEagerLinearRoundTimer(t *testing.T) {
stop()
}

func TestExponentialRoundTimer(t *testing.T) {
func TestLinearRoundTimer(t *testing.T) {
tests := []struct {
name string
round int64
Expand All @@ -125,12 +125,12 @@ func TestExponentialRoundTimer(t *testing.T) {
{
name: "round 2",
round: 2,
want: 200 * time.Millisecond,
want: 400 * time.Millisecond,
},
{
name: "round 3",
round: 3,
want: 400 * time.Millisecond,
want: 600 * time.Millisecond,
},
{
name: "round 4",
Expand All @@ -141,7 +141,7 @@ func TestExponentialRoundTimer(t *testing.T) {

for _, tt := range tests {
fakeClock := clockwork.NewFakeClock()
timer := utils.NewExponentialRoundTimerWithClock(fakeClock)
timer := utils.NewLinearRoundTimerWithClock(fakeClock)

t.Run(tt.name, func(t *testing.T) {
// Start the timerType
Expand Down Expand Up @@ -170,17 +170,28 @@ func TestGetTimerFunc(t *testing.T) {
require.Equal(t, utils.TimerEagerDoubleLinear, timerFunc(core.NewAttesterDuty(2)).Type())

featureset.DisableForT(t, featureset.EagerDoubleLinear)
featureset.EnableForT(t, featureset.Exponential)

timerFunc = utils.GetTimerFunc()
require.Equal(t, utils.TimerExponential, timerFunc(core.NewAttesterDuty(0)).Type())
require.Equal(t, utils.TimerExponential, timerFunc(core.NewAttesterDuty(1)).Type())
require.Equal(t, utils.TimerExponential, timerFunc(core.NewAttesterDuty(2)).Type())
require.Equal(t, utils.TimerIncreasing, timerFunc(core.NewAttesterDuty(0)).Type())
require.Equal(t, utils.TimerIncreasing, timerFunc(core.NewAttesterDuty(1)).Type())
require.Equal(t, utils.TimerIncreasing, timerFunc(core.NewAttesterDuty(2)).Type())

featureset.DisableForT(t, featureset.Exponential)
featureset.EnableForT(t, featureset.Linear)

timerFunc = utils.GetTimerFunc()
// non proposer duty, defaults to increasing
require.Equal(t, utils.TimerIncreasing, timerFunc(core.NewAttesterDuty(0)).Type())
require.Equal(t, utils.TimerIncreasing, timerFunc(core.NewAttesterDuty(1)).Type())
require.Equal(t, utils.TimerIncreasing, timerFunc(core.NewAttesterDuty(2)).Type())

featureset.EnableForT(t, featureset.EagerDoubleLinear)
// non proposer duty, defaults to eager
require.Equal(t, utils.TimerEagerDoubleLinear, timerFunc(core.NewAttesterDuty(0)).Type())
require.Equal(t, utils.TimerEagerDoubleLinear, timerFunc(core.NewAttesterDuty(1)).Type())
require.Equal(t, utils.TimerEagerDoubleLinear, timerFunc(core.NewAttesterDuty(2)).Type())

// proposer duty, uses linear
require.Equal(t, utils.TimerLinear, timerFunc(core.NewProposerDuty(0)).Type())
require.Equal(t, utils.TimerLinear, timerFunc(core.NewProposerDuty(1)).Type())
require.Equal(t, utils.TimerLinear, timerFunc(core.NewProposerDuty(2)).Type())
}
4 changes: 2 additions & 2 deletions docs/consensus.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ The `IncreasingRoundTimer` uses a linear increment strategy for round durations.

The `EagerDoubleLinearRoundTimer` aligns start times across participants by starting at an absolute time and doubling the round duration when the leader is active. The round duration increases linearly according to `LinearRoundInc`. This aims to fix an issue with the original solution where the leader resets the timer at the start of the round while others reset when they receive the justified pre-prepare which leads to leaders getting out of sync with the rest. This timer is enabled by default and doesn't require additional flags.

### Exponential Round Timer
### Linear Round Timer

The `ExponentialRoundTimer` increases round durations exponentially. It provides a sufficient timeout for the initial round and grows from a smaller base timeout for subsequent rounds. The idea behind this timer is, that the consensus timeout includes fetching the signing data. As all nodes do that at the start, irregardless if they are leader or not, after the first timeout, the remaining nodes already had time to fetch their signing data. Therefore they won't need as much time to reach consensus as the leader for the first round did. The shorter subsequent rounds allow us to more quickly skip underperforming leader when compared to both `IncreasingRoundTimer` and `EagerDoubleLinearRoundTimer`, giving more leaders a chance to advance the protocol before its too late. To enable this timer, use the flag `--feature-set-enable "exponential"`. Since this timer has precedence over the `EagerDoubleLinearRoundTimer` there is no need to disable the default timer.
The `LinearRoundTimer` increases round durations linearly. It provides a sufficient timeout for the initial round and grows from a smaller base timeout for subsequent rounds. The idea behind this timer is, that the consensus timeout includes fetching the signing data. As all nodes do that at the start, irregardless if they are leader or not, after the first timeout, the remaining nodes already had time to fetch their signing data. Therefore they won't need as much time to reach consensus as the leader for the first round did. The shorter subsequent rounds allow us to more quickly skip underperforming leader when compared to both `IncreasingRoundTimer` and `EagerDoubleLinearRoundTimer`, giving more leaders a chance to advance the protocol before its too late. This timer only affects Proposer duties, for the remaning ones it fallbacks to either `EagerDoubleLinearRoundTimer` or `IncreasingRoundTimer` depending on feature set flags. To enable it, use the flag `--feature-set-enable "linear"`. Since this timer has precedence over the `EagerDoubleLinearRoundTimer` there is no need to disable the default timer.

## Observability

Expand Down
Loading