Skip to content

Commit 3d45338

Browse files
authored
core/consensus: add longer proposer duty timeout (#3739)
Increase first round timeout value for proposer duty to 1.5s across all timers category: feature ticket: #3430 feature_flag: proposal_timeout
1 parent b2da103 commit 3d45338

File tree

3 files changed

+180
-16
lines changed

3 files changed

+180
-16
lines changed

app/featureset/featureset.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ const (
5757
// Previously this was the default behaviour, however, tracking on-chain inclusions post-electra is costly.
5858
// The extra load that Charon puts the beacon node is deemed so high that it can throttle the completion of other duties.
5959
AttestationInclusion Feature = "attestation_inclusion"
60+
61+
// ProposalTimeout enables a longer first consensus round timeout of 1.5 seconds for proposal duty.
62+
ProposalTimeout = "proposal_timeout"
6063
)
6164

6265
var (
@@ -71,6 +74,7 @@ var (
7174
Linear: statusAlpha,
7275
SSEReorgDuties: statusAlpha,
7376
AttestationInclusion: statusAlpha,
77+
ProposalTimeout: statusAlpha,
7478
// Add all features and there status here.
7579
}
7680

core/consensus/utils/roundtimer.go

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,21 @@ func GetTimerFunc() TimerFunc {
2828
return func(duty core.Duty) RoundTimer {
2929
// Linear timer only affects Proposer duty
3030
if duty.Type == core.DutyProposer {
31-
return NewLinearRoundTimer()
31+
return NewLinearRoundTimerWithDuty(duty)
3232
} else if featureset.Enabled(featureset.EagerDoubleLinear) {
33-
return NewDoubleEagerLinearRoundTimer()
33+
return NewDoubleEagerLinearRoundTimerWithDuty(duty)
3434
}
3535

36-
return NewIncreasingRoundTimer()
36+
return NewIncreasingRoundTimerWithDuty(duty)
3737
}
3838
}
3939

4040
if featureset.Enabled(featureset.EagerDoubleLinear) {
41-
return func(core.Duty) RoundTimer {
42-
return NewDoubleEagerLinearRoundTimer()
43-
}
41+
return NewDoubleEagerLinearRoundTimerWithDuty
4442
}
4543

4644
// Default to increasing round timer.
47-
return func(core.Duty) RoundTimer {
48-
return NewIncreasingRoundTimer()
49-
}
45+
return NewIncreasingRoundTimerWithDuty
5046
}
5147

5248
// TimerType is the type of round timer.
@@ -82,6 +78,12 @@ type RoundTimer interface {
8278
Type() TimerType
8379
}
8480

81+
// proposalTimeoutOptimization returns true if ProposalTimeout feature is enabled, the duty is proposer and
82+
// we are in the first round.
83+
func proposalTimeoutOptimization(duty core.Duty, round int64) bool {
84+
return featureset.Enabled(featureset.ProposalTimeout) && duty.Type == core.DutyProposer && round == 1
85+
}
86+
8587
// NewTimeoutRoundTimer returns a new increasing round timer type.
8688
func NewIncreasingRoundTimer() RoundTimer {
8789
return NewIncreasingRoundTimerWithClock(clockwork.NewRealClock())
@@ -94,17 +96,40 @@ func NewIncreasingRoundTimerWithClock(clock clockwork.Clock) RoundTimer {
9496
}
9597
}
9698

99+
// NewIncreasingRoundTimerWithDuty returns a new eager double linear round timer type for a specific duty.
100+
func NewIncreasingRoundTimerWithDuty(duty core.Duty) RoundTimer {
101+
return &increasingRoundTimer{
102+
clock: clockwork.NewRealClock(),
103+
duty: duty,
104+
}
105+
}
106+
107+
// NewIncreasingRoundTimerWithDuty returns a new eager double linear round timer type for a specific duty and custom clock.
108+
func NewIncreasingRoundTimerWithDutyAndClock(duty core.Duty, clock clockwork.Clock) RoundTimer {
109+
return &increasingRoundTimer{
110+
clock: clock,
111+
duty: duty,
112+
}
113+
}
114+
97115
// increasingRoundTimer implements a linear increasing round timerType.
98116
type increasingRoundTimer struct {
99117
clock clockwork.Clock
118+
duty core.Duty
100119
}
101120

102121
func (increasingRoundTimer) Type() TimerType {
103122
return TimerIncreasing
104123
}
105124

106125
func (t increasingRoundTimer) Timer(round int64) (<-chan time.Time, func()) {
107-
timer := t.clock.NewTimer(increasingRoundTimeout(round))
126+
timeout := increasingRoundTimeout(round)
127+
if proposalTimeoutOptimization(t.duty, round) {
128+
timeout = 1500 * time.Millisecond
129+
}
130+
131+
timer := t.clock.NewTimer(timeout)
132+
108133
return timer.Chan(), func() { timer.Stop() }
109134
}
110135

@@ -121,6 +146,24 @@ func NewDoubleEagerLinearRoundTimerWithClock(clock clockwork.Clock) RoundTimer {
121146
}
122147
}
123148

149+
// NewDoubleEagerLinearRoundTimerWithDuty returns a new eager double linear round timer type for a specific duty.
150+
func NewDoubleEagerLinearRoundTimerWithDuty(duty core.Duty) RoundTimer {
151+
return &doubleEagerLinearRoundTimer{
152+
clock: clockwork.NewRealClock(),
153+
duty: duty,
154+
firstDeadlines: make(map[int64]time.Time),
155+
}
156+
}
157+
158+
// NewDoubleEagerLinearRoundTimerWithDutyAndClock returns a new eager double linear round timer type for a specific duty and custom clock.
159+
func NewDoubleEagerLinearRoundTimerWithDutyAndClock(duty core.Duty, clock clockwork.Clock) RoundTimer {
160+
return &doubleEagerLinearRoundTimer{
161+
clock: clock,
162+
duty: duty,
163+
firstDeadlines: make(map[int64]time.Time),
164+
}
165+
}
166+
124167
// doubleEagerLinearRoundTimer implements a round timerType with the following properties:
125168
//
126169
// It doubles the round duration when a leader is active.
@@ -137,6 +180,7 @@ func NewDoubleEagerLinearRoundTimerWithClock(clock clockwork.Clock) RoundTimer {
137180
// It is linear, meaning the round duration increases linearly with the round number: 1s, 2s, 3s, etc.
138181
type doubleEagerLinearRoundTimer struct {
139182
clock clockwork.Clock
183+
duty core.Duty
140184

141185
mu sync.Mutex
142186
firstDeadlines map[int64]time.Time
@@ -150,13 +194,20 @@ func (t *doubleEagerLinearRoundTimer) Timer(round int64) (<-chan time.Time, func
150194
t.mu.Lock()
151195
defer t.mu.Unlock()
152196

197+
var timeout time.Duration
198+
if proposalTimeoutOptimization(t.duty, round) {
199+
timeout = 1500 * time.Millisecond
200+
} else {
201+
timeout = linearRoundTimeout(round)
202+
}
203+
153204
var deadline time.Time
154205
if first, ok := t.firstDeadlines[round]; ok {
155206
// Deadline is either double the first timeout
156-
deadline = first.Add(linearRoundTimeout(round))
207+
deadline = first.Add(timeout)
157208
} else {
158209
// Or the first timeout
159-
deadline = t.clock.Now().Add(linearRoundTimeout(round))
210+
deadline = t.clock.Now().Add(timeout)
160211
t.firstDeadlines[round] = deadline
161212
}
162213

@@ -173,22 +224,27 @@ func (t *doubleEagerLinearRoundTimer) Timer(round int64) (<-chan time.Time, func
173224
// which will increase linearly
174225
type linearRoundTimer struct {
175226
clock clockwork.Clock
227+
duty core.Duty
176228
}
177229

178230
func (*linearRoundTimer) Type() TimerType {
179231
return TimerLinear
180232
}
181233

182234
func (t *linearRoundTimer) Timer(round int64) (<-chan time.Time, func()) {
183-
var timer clockwork.Timer
184-
if round == 1 {
235+
var timeout time.Duration
236+
if proposalTimeoutOptimization(t.duty, round) {
237+
timeout = 1500 * time.Millisecond
238+
} else if round == 1 {
185239
// First round has 1 second
186-
timer = t.clock.NewTimer(time.Second)
240+
timeout = time.Second
187241
} else {
188242
// Subsequent rounds have linearly more time starting at 400 milliseconds
189-
timer = t.clock.NewTimer(time.Duration(200*(round-1) + 200))
243+
timeout = time.Duration(200*(round-1) + 200)
190244
}
191245

246+
timer := t.clock.NewTimer(timeout)
247+
192248
return timer.Chan(), func() { timer.Stop() }
193249
}
194250

@@ -197,8 +253,25 @@ func NewLinearRoundTimer() RoundTimer {
197253
return NewLinearRoundTimerWithClock(clockwork.NewRealClock())
198254
}
199255

256+
// NewLinearRoundTimerWithClock returns a new linear round timer type with a custom clock.
200257
func NewLinearRoundTimerWithClock(clock clockwork.Clock) RoundTimer {
201258
return &linearRoundTimer{
202259
clock: clock,
203260
}
204261
}
262+
263+
// NewLinearRoundTimerWithDuty returns a new linear round timer type for a specific duty.
264+
func NewLinearRoundTimerWithDuty(duty core.Duty) RoundTimer {
265+
return &linearRoundTimer{
266+
clock: clockwork.NewRealClock(),
267+
duty: duty,
268+
}
269+
}
270+
271+
// NewLinearRoundTimerWithDutyAndClock returns a new linear round timer type for a specific duty and custom clock.
272+
func NewLinearRoundTimerWithDutyAndClock(duty core.Duty, clock clockwork.Clock) RoundTimer {
273+
return &linearRoundTimer{
274+
clock: clock,
275+
duty: duty,
276+
}
277+
}

core/consensus/utils/roundtimer_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,90 @@ func TestGetTimerFunc(t *testing.T) {
195195
require.Equal(t, utils.TimerLinear, timerFunc(core.NewProposerDuty(1)).Type())
196196
require.Equal(t, utils.TimerLinear, timerFunc(core.NewProposerDuty(2)).Type())
197197
}
198+
199+
func TestProposalTimeoutOptimizationIncreasingRoundTimer(t *testing.T) {
200+
featureset.EnableForT(t, featureset.ProposalTimeout)
201+
defer featureset.DisableForT(t, featureset.ProposalTimeout)
202+
203+
fakeClock := clockwork.NewFakeClock()
204+
duty := core.NewProposerDuty(0)
205+
timer := utils.NewIncreasingRoundTimerWithDutyAndClock(duty, fakeClock)
206+
207+
// First round for proposer should be 1.5s
208+
timerC, stop := timer.Timer(1)
209+
fakeClock.Advance(1500 * time.Millisecond)
210+
select {
211+
case <-timerC:
212+
default:
213+
require.Fail(t, "Timer(round 1, proposer) did not fire at 1.5s")
214+
}
215+
stop()
216+
217+
// Second round should use original logic
218+
timerC, stop = timer.Timer(2)
219+
fakeClock.Advance(utils.IncRoundStart + 2*utils.IncRoundIncrease)
220+
select {
221+
case <-timerC:
222+
default:
223+
require.Fail(t, "Timer(round 2, proposer) did not fire at original duration")
224+
}
225+
stop()
226+
}
227+
228+
func TestProposalTimeoutOptimizationDoubleEagerLinearRoundTimer(t *testing.T) {
229+
featureset.EnableForT(t, featureset.ProposalTimeout)
230+
defer featureset.DisableForT(t, featureset.ProposalTimeout)
231+
232+
fakeClock := clockwork.NewFakeClock()
233+
duty := core.NewProposerDuty(0)
234+
timer := utils.NewDoubleEagerLinearRoundTimerWithDutyAndClock(duty, fakeClock)
235+
236+
// First round for proposer should be 1.5s
237+
timerC, stop := timer.Timer(1)
238+
fakeClock.Advance(1500 * time.Millisecond)
239+
select {
240+
case <-timerC:
241+
default:
242+
require.Fail(t, "Timer(round 1, proposer) did not fire at 1.5s")
243+
}
244+
stop()
245+
246+
// Second round should use original logic (2s)
247+
timerC, stop = timer.Timer(2)
248+
fakeClock.Advance(2 * time.Second)
249+
select {
250+
case <-timerC:
251+
default:
252+
require.Fail(t, "Timer(round 2, proposer) did not fire at 2s")
253+
}
254+
stop()
255+
}
256+
257+
func TestProposalTimeoutOptimizationLinearRoundTimer(t *testing.T) {
258+
featureset.EnableForT(t, featureset.ProposalTimeout)
259+
defer featureset.DisableForT(t, featureset.ProposalTimeout)
260+
261+
fakeClock := clockwork.NewFakeClock()
262+
duty := core.NewProposerDuty(0)
263+
timer := utils.NewLinearRoundTimerWithDutyAndClock(duty, fakeClock)
264+
265+
// First round for proposer should be 1.5s
266+
timerC, stop := timer.Timer(1)
267+
fakeClock.Advance(1500 * time.Millisecond)
268+
select {
269+
case <-timerC:
270+
default:
271+
require.Fail(t, "Timer(round 1, proposer) did not fire at 1.5s")
272+
}
273+
stop()
274+
275+
// Third round should use original logic (600ms)
276+
timerC, stop = timer.Timer(3)
277+
fakeClock.Advance(600 * time.Millisecond)
278+
select {
279+
case <-timerC:
280+
default:
281+
require.Fail(t, "Timer(round 3, proposer) did not fire at 600ms")
282+
}
283+
stop()
284+
}

0 commit comments

Comments
 (0)