diff --git a/f3_test.go b/f3_test.go index a1b7177b..a1212e3e 100644 --- a/f3_test.go +++ b/f3_test.go @@ -261,6 +261,7 @@ var base = manifest.Manifest{ Gpbft: manifest.GpbftConfig{ Delta: 3 * time.Second, DeltaBackOffExponent: 1.3, + DeltaBackOffMax: 1 * time.Hour, QualityDeltaMultiplier: 1.0, MaxLookaheadRounds: 5, ChainProposedLength: 30, diff --git a/gpbft/gpbft.go b/gpbft/gpbft.go index 9b57bd77..c6b72051 100644 --- a/gpbft/gpbft.go +++ b/gpbft/gpbft.go @@ -964,11 +964,7 @@ func (i *instance) alarmAfterSynchrony() time.Time { // The delay duration increases with each round. // Returns the absolute time at which the alarm will fire. func (i *instance) alarmAfterSynchronyWithMulti(multi float64) time.Time { - delta := time.Duration(float64(i.participant.delta) * multi * - math.Pow(i.participant.deltaBackOffExponent, float64(i.current.Round))) - timeout := i.participant.host.Time().Add(2 * delta) - i.participant.host.SetAlarm(timeout) - return timeout + return i.participant.AlarmAfterSynchronyWithMulti(i.current.Round, multi) } // Builds a justification for a value from a quorum result. diff --git a/gpbft/options.go b/gpbft/options.go index 049af71e..644b58f1 100644 --- a/gpbft/options.go +++ b/gpbft/options.go @@ -11,6 +11,7 @@ import ( const ( defaultDelta = 3 * time.Second defaultDeltaBackOffExponent = 2.0 + defaultDeltaBackOffMax = 1 * time.Hour defaultMaxCachedInstances = 10 defaultMaxCachedMessagesPerInstance = 25_000 defaultCommitteeLookback = 10 @@ -22,6 +23,7 @@ type Option func(*options) error type options struct { delta time.Duration deltaBackOffExponent float64 + deltaBackOffMax time.Duration qualityDeltaMulti float64 @@ -41,6 +43,7 @@ func newOptions(o ...Option) (*options, error) { opts := &options{ delta: defaultDelta, deltaBackOffExponent: defaultDeltaBackOffExponent, + deltaBackOffMax: defaultDeltaBackOffMax, qualityDeltaMulti: 1.0, committeeLookback: defaultCommitteeLookback, rebroadcastAfter: defaultRebroadcastAfter, @@ -87,6 +90,20 @@ func WithDeltaBackOffExponent(e float64) Option { } } +// WithDeltaBackOffMax sets the delta back-off max for each round. +// Defaults to 1h if unspecified. It must be larger than zero. +// +// See: https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0086.md#synchronization-of-participants-in-the-current-instance +func WithDeltaBackOffMax(d time.Duration) Option { + return func(o *options) error { + if d < 0 { + return errors.New("delta duration max cannot be less than zero") + } + o.deltaBackOffMax = d + return nil + } +} + func WithQualityDeltaMultiplier(m float64) Option { return func(o *options) error { if m < 0 { diff --git a/gpbft/participant.go b/gpbft/participant.go index add8a3f5..7be2695f 100644 --- a/gpbft/participant.go +++ b/gpbft/participant.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "sort" "sync" "time" @@ -315,6 +316,20 @@ func (p *Participant) trace(format string, args ...any) { } } +// Sets an alarm to be delivered after a synchrony delay including a multiplier on the duration. +// The delay duration increases with each round. +// Returns the absolute time at which the alarm will fire. +func (p *Participant) AlarmAfterSynchronyWithMulti(round uint64, multi float64) time.Time { + delta := time.Duration(float64(p.delta) * multi * + math.Pow(p.deltaBackOffExponent, float64(round))) + if delta > p.deltaBackOffMax { + delta = p.deltaBackOffMax + } + timeout := p.host.Time().Add(2 * delta) + p.host.SetAlarm(timeout) + return timeout +} + // A collection of messages queued for delivery for a future instance. // The queue drops equivocations and unjustified messages beyond some round number. type messageQueue struct { diff --git a/gpbft/participant_test.go b/gpbft/participant_test.go index fcddbc6f..38f60226 100644 --- a/gpbft/participant_test.go +++ b/gpbft/participant_test.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "math" "math/rand" "sync" "testing" @@ -54,6 +55,7 @@ func newParticipantTestSubject(t *testing.T, seed int64, instance uint64) *parti const ( delta = 2 * time.Second deltaBackOffExponent = 1.3 + deltaBackOffMax = 30 * time.Second ) rng := rand.New(rand.NewSource(seed)) @@ -89,7 +91,7 @@ func newParticipantTestSubject(t *testing.T, seed int64, instance uint64) *parti subject.Participant, err = gpbft.NewParticipant(subject.host, gpbft.WithTracer(subject), gpbft.WithDelta(delta), - gpbft.WithDeltaBackOffExponent(deltaBackOffExponent)) + gpbft.WithDeltaBackOffExponent(deltaBackOffExponent), gpbft.WithDeltaBackOffMax(deltaBackOffMax)) require.NoError(t, err) subject.requireNotStarted() return subject @@ -485,6 +487,54 @@ func TestParticipant(t *testing.T) { }) } +func TestParticipant_alarmAfterSynchronyWithMulti(t *testing.T) { + const ( + seed = 894651320 + delta = 2 * time.Second + deltaBackOffExponent = 1.2 + deltaBackOffMax = 30 * time.Second + ) + + now := time.Now() + + tests := []struct { + name string + round uint64 + multi float64 + timeout time.Duration + }{ + { + name: "uncapped", + round: 3, + multi: 1, + timeout: 2 * time.Duration(float64(delta)*1* + math.Pow(deltaBackOffExponent, float64(3))), + }, + { + name: "capped", + round: 30, + multi: 1, + timeout: 2 * deltaBackOffMax, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + host := gpbft.NewMockHost(t) + host.On("NetworkName").Return(gpbft.NetworkName("testnetnet")).Maybe() + host.On("Time").Return(now) + host.On("SetAlarm", mock.Anything) + p, err := gpbft.NewParticipant(host, + gpbft.WithDelta(delta), + gpbft.WithDeltaBackOffExponent(deltaBackOffExponent), gpbft.WithDeltaBackOffMax(deltaBackOffMax)) + require.NoError(t, err) + require.NotNil(t, p) + timeout := p.AlarmAfterSynchronyWithMulti(test.round, test.multi) + require.Equal(t, timeout, now.Add(test.timeout)) + }) + } +} + func TestParticipant_ValidateMessage(t *testing.T) { const ( seed = 894651320 diff --git a/manifest/manifest.go b/manifest/manifest.go index 90ffc41e..51a92322 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -36,6 +36,7 @@ var ( DefaultGpbftConfig = GpbftConfig{ Delta: 6 * time.Second, DeltaBackOffExponent: 2.0, + DeltaBackOffMax: 1 * time.Hour, QualityDeltaMultiplier: 1.0, MaxLookaheadRounds: 5, ChainProposedLength: gpbft.ChainDefaultLen, @@ -114,6 +115,7 @@ func (c *CxConfig) Validate() error { type GpbftConfig struct { Delta time.Duration DeltaBackOffExponent float64 + DeltaBackOffMax time.Duration QualityDeltaMultiplier float64 MaxLookaheadRounds uint64 @@ -132,6 +134,9 @@ func (g *GpbftConfig) Validate() error { if g.DeltaBackOffExponent < 1.0 { return fmt.Errorf("gpbft backoff exponent must be at least 1.0, was %f", g.DeltaBackOffExponent) } + if g.DeltaBackOffMax < g.Delta { + return fmt.Errorf("gpbft delta backoff max must be no less than Delta, was %s", g.DeltaBackOffMax) + } if g.QualityDeltaMultiplier < 0 { return fmt.Errorf("gpbft quality duration multiplier is negative: %f", g.QualityDeltaMultiplier) @@ -161,6 +166,7 @@ func (g *GpbftConfig) ToOptions() []gpbft.Option { return []gpbft.Option{ gpbft.WithDelta(g.Delta), gpbft.WithDeltaBackOffExponent(g.DeltaBackOffExponent), + gpbft.WithDeltaBackOffMax(g.DeltaBackOffMax), gpbft.WithQualityDeltaMultiplier(g.QualityDeltaMultiplier), gpbft.WithMaxLookaheadRounds(g.MaxLookaheadRounds), gpbft.WithRebroadcastBackoff( diff --git a/manifest/manifest_test.go b/manifest/manifest_test.go index 35aaf6fb..98cccfbe 100644 --- a/manifest/manifest_test.go +++ b/manifest/manifest_test.go @@ -16,8 +16,9 @@ var base = manifest.Manifest{ NetworkName: "test", CommitteeLookback: 10, Gpbft: manifest.GpbftConfig{ - Delta: 10, + Delta: 10 * time.Second, DeltaBackOffExponent: 1.2, + DeltaBackOffMax: 1 * time.Hour, QualityDeltaMultiplier: 1.0, MaxLookaheadRounds: 5, ChainProposedLength: gpbft.ChainDefaultLen, @@ -113,8 +114,8 @@ func TestManifest_CID(t *testing.T) { t.Parallel() const ( - wantLocalDevnetCid = "baguqfiheaiqgpujeb5upzhbblchkuc2sxeis2y5upbsefktyspqqmjrcr27fiua" - wantAfterUpdateCid = "baguqfiheaiqaixtgfxvyaqdkdy6nsdybpvwjw7vgtw22enjg24kunho3b5nrduq" + wantLocalDevnetCid = "baguqfiheaiqkwe3ngxsexltkudtgs3ssqdeney2gxugqmw3jskkdmg56laxwzxq" + wantAfterUpdateCid = "baguqfiheaiqpmprgeaqongk4wunqi5rez2jpvsxhg3cebi5pvtjpbyz5gfrfvgq" ) subject := manifest.LocalDevnetManifest() // Use a fixed network name for deterministic CID calculation.