Skip to content

Commit

Permalink
core/consensus: add new consensus round timer (#3440)
Browse files Browse the repository at this point in the history
Add a third consensus round timer which implements the following behavior:
- First consensus round has 1 second to complete
- Subsequent rounds have exponentially more time starting at 200ms (400ms, 800ms, 1.6s, etc...)

category: feature
ticket: #3430
feature_flag: exponential
  • Loading branch information
DiogoSantoss committed Jan 15, 2025
1 parent 72523aa commit 2cbc8d2
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 6 deletions.
5 changes: 5 additions & 0 deletions app/featureset/featureset.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ const (
// The feature gets automatically enabled when the current network is gnosis|chiado,
// unless the user disabled this feature explicitly.
GnosisBlockHotfix Feature = "gnosis_block_hotfix"

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

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

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

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

import (
"math"
"strings"
"sync"
"time"
Expand All @@ -24,6 +25,12 @@ 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.EagerDoubleLinear) {
return func(core.Duty) RoundTimer {
return NewDoubleEagerLinearRoundTimer()
Expand All @@ -47,6 +54,7 @@ func (t TimerType) Eager() bool {
const (
TimerIncreasing TimerType = "inc"
TimerEagerDoubleLinear TimerType = "eager_dlinear"
TimerExponential TimerType = "exponential"
)

// increasingRoundTimeout returns the duration for a round that starts at incRoundStart in round 1
Expand Down Expand Up @@ -150,3 +158,41 @@ 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:
//
// 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 {
clock clockwork.Clock
}

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

func (t *exponentialRoundTimer) 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))
}

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

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

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

func TestExponentialRoundTimer(t *testing.T) {
tests := []struct {
name string
round int64
want time.Duration
}{
{
name: "round 1",
round: 1,
want: 1000 * time.Millisecond,
},
{
name: "round 2",
round: 2,
want: 200 * time.Millisecond,
},
{
name: "round 3",
round: 3,
want: 400 * time.Millisecond,
},
{
name: "round 4",
round: 4,
want: 800 * time.Millisecond,
},
}

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

t.Run(tt.name, func(t *testing.T) {
// Start the timerType
timerC, stop := timer.Timer(tt.round)

// Advance the fake clock
fakeClock.Advance(tt.want)

// Check if the timerType fires
select {
case <-timerC:
default:
require.Fail(t, "Fail", "Timer(round %d) did not fire, want %v", tt.round, tt.want)
}

// Stop the timerType
stop()
})
}
}

func TestGetTimerFunc(t *testing.T) {
timerFunc := utils.GetTimerFunc()
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())

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())

featureset.DisableForT(t, featureset.Exponential)

timerFunc = utils.GetTimerFunc()
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())
Expand Down
30 changes: 24 additions & 6 deletions docs/consensus.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ The input to the Priority protocol is a list of protocols defined in order of pr

```json
[
"/charon/consensus/hotstuff/1.0.0", // Highest precedence
"/charon/consensus/abft/2.0.0",
"/charon/consensus/abft/1.0.0",
"/charon/consensus/qbft/2.0.0", // Lowest precedence and the fallback since it is always present
"/charon/consensus/hotstuff/1.0.0", // Highest precedence
"/charon/consensus/abft/2.0.0",
"/charon/consensus/abft/1.0.0",
"/charon/consensus/qbft/2.0.0" // Lowest precedence and the fallback since it is always present
]
```

The output of the Priority protocol is the common "subset" of all inputs, respecting the initial order of precedence, e.g.:

```json
[
"/charon/consensus/abft/1.0.0", // This means the majority of nodes have this protocol available
"/charon/consensus/qbft/2.0.0",
"/charon/consensus/abft/1.0.0", // This means the majority of nodes have this protocol available
"/charon/consensus/qbft/2.0.0"
]
```

Expand All @@ -53,6 +53,24 @@ To list all available consensus protocols (with versions), a user can run the co

When a node starts, it sequentially mutates the list of preferred consensus protocols by processing the cluster configuration file and then the mentioned CLI flag. The final list of preferred protocols is then passed to the Priority protocol for cluster-wide consensus. Until the Priority protocol reaches consensus, the cluster will use the default QBFT v2.0 protocol for any duties.

## Consensus Round Duration

There are three different round timer implementations. These timers define the duration of each consensus round and how the timing adjusts for subsequent rounds. All nodes in a cluster must use the same timer implementation to ensure proper consensus operation.

The default implementation is the `EagerDoubleLinearRoundTimer`, which is recommended for most deployments. Other round timers can be enabled or disabled by using the `--feature-set-enable <timer-name>` and `--feature-set-disable <timer-name>` flags.

### Increasing Round Timer

The `IncreasingRoundTimer` uses a linear increment strategy for round durations. It starts with a base duration and increases by a fixed increment for each subsequent round. The formula for a given round `n` is `Duration = IncRoundStart + (IncRoundIncrease * n)`. To enable this timer, disable the default timer by using the flag `--feature-set-disable "eager_double_linear"`.

### Eager Double Linear Round Timer

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

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.

## Observability

The four existing metrics are reflecting the consensus layer behavior:
Expand Down

0 comments on commit 2cbc8d2

Please sign in to comment.