diff --git a/internal/consensus/state.go b/internal/consensus/state.go index 8bf6a8290..d8f3122c5 100644 --- a/internal/consensus/state.go +++ b/internal/consensus/state.go @@ -1400,7 +1400,7 @@ func (cs *State) proposalIsTimely() bool { MessageDelay: cs.state.ConsensusParams.Synchrony.MessageDelay, } - return cs.Proposal.IsTimely(cs.ProposalReceiveTime, sp) + return cs.Proposal.IsTimely(cs.ProposalReceiveTime, sp, cs.Round) } func (cs *State) defaultDoPrevote(ctx context.Context, height int64, round int32) { @@ -2570,7 +2570,7 @@ func (cs *State) calculateProposalTimestampDifferenceMetric() { MessageDelay: cs.state.ConsensusParams.Synchrony.MessageDelay, } - isTimely := cs.Proposal.IsTimely(cs.ProposalReceiveTime, tp) + isTimely := cs.Proposal.IsTimely(cs.ProposalReceiveTime, tp, cs.Round) cs.metrics.ProposalTimestampDifference.With("is_timely", fmt.Sprintf("%t", isTimely)). Observe(cs.ProposalReceiveTime.Sub(cs.Proposal.Timestamp).Seconds()) } diff --git a/types/proposal.go b/types/proposal.go index 87171ff46..3c806f657 100644 --- a/types/proposal.go +++ b/types/proposal.go @@ -3,6 +3,7 @@ package types import ( "errors" "fmt" + "math" "time" "github.com/tendermint/tendermint/internal/libs/protoio" @@ -89,11 +90,19 @@ func (p *Proposal) ValidateBasic() error { // // For more information on the meaning of 'timely', see the proposer-based timestamp specification: // https://github.com/tendermint/spec/tree/master/spec/consensus/proposer-based-timestamp -func (p *Proposal) IsTimely(recvTime time.Time, sp SynchronyParams) bool { +func (p *Proposal) IsTimely(recvTime time.Time, sp SynchronyParams, round int32) bool { + // The message delay values are scaled as rounds progress. + // Every 10 rounds, the message delay is doubled to allow consensus to + // proceed in the case that the choosen value was too small for the given network conditions. + // For more information and discussion on this mechanism, see the relevant github issue: + // https://github.com/tendermint/spec/issues/371 + roundModifier := time.Duration(math.Exp2(float64(round / 10))) + msgDelay := sp.MessageDelay * roundModifier + // lhs is `proposedBlockTime - Precision` in the first inequality lhs := p.Timestamp.Add(-sp.Precision) // rhs is `proposedBlockTime + MsgDelay + Precision` in the second inequality - rhs := p.Timestamp.Add(sp.MessageDelay).Add(sp.Precision) + rhs := p.Timestamp.Add(msgDelay).Add(sp.Precision) if recvTime.Before(lhs) || recvTime.After(rhs) { return false diff --git a/types/proposal_test.go b/types/proposal_test.go index fc99a3884..abfff8e7f 100644 --- a/types/proposal_test.go +++ b/types/proposal_test.go @@ -216,54 +216,61 @@ func TestIsTimely(t *testing.T) { genesisTime, err := time.Parse(time.RFC3339, "2019-03-13T23:00:00Z") require.NoError(t, err) testCases := []struct { - name string - proposalHeight int64 - proposalTime time.Time - recvTime time.Time - precision time.Duration - msgDelay time.Duration - expectTimely bool + name string + proposalTime time.Time + recvTime time.Time + precision time.Duration + msgDelay time.Duration + expectTimely bool + round int32 }{ // proposalTime - precision <= localTime <= proposalTime + msgDelay + precision { // Checking that the following inequality evaluates to true: // 0 - 2 <= 1 <= 0 + 1 + 2 - name: "basic timely", - proposalHeight: 2, - proposalTime: genesisTime, - recvTime: genesisTime.Add(1 * time.Nanosecond), - precision: time.Nanosecond * 2, - msgDelay: time.Nanosecond, - expectTimely: true, + name: "basic timely", + proposalTime: genesisTime, + recvTime: genesisTime.Add(1 * time.Nanosecond), + precision: time.Nanosecond * 2, + msgDelay: time.Nanosecond, + expectTimely: true, }, { // Checking that the following inequality evaluates to false: // 0 - 2 <= 4 <= 0 + 1 + 2 - name: "local time too large", - proposalHeight: 2, - proposalTime: genesisTime, - recvTime: genesisTime.Add(4 * time.Nanosecond), - precision: time.Nanosecond * 2, - msgDelay: time.Nanosecond, - expectTimely: false, + name: "local time too large", + proposalTime: genesisTime, + recvTime: genesisTime.Add(4 * time.Nanosecond), + precision: time.Nanosecond * 2, + msgDelay: time.Nanosecond, + expectTimely: false, }, { // Checking that the following inequality evaluates to false: // 4 - 2 <= 0 <= 4 + 2 + 1 - name: "proposal time too large", - proposalHeight: 2, - proposalTime: genesisTime.Add(4 * time.Nanosecond), - recvTime: genesisTime, - precision: time.Nanosecond * 2, - msgDelay: time.Nanosecond, - expectTimely: false, + name: "proposal time too large", + proposalTime: genesisTime.Add(4 * time.Nanosecond), + recvTime: genesisTime, + precision: time.Nanosecond * 2, + msgDelay: time.Nanosecond, + expectTimely: false, + }, + { + // Checking that the following inequality evaluates to true: + // 0 - (2 * 2) <= 4 <= 0 + (1 * 2) + 2 + name: "message delay adapts after 10 rounds", + proposalTime: genesisTime, + recvTime: genesisTime.Add(4 * time.Nanosecond), + precision: time.Nanosecond * 2, + msgDelay: time.Nanosecond, + expectTimely: true, + round: 10, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { p := Proposal{ - Height: testCase.proposalHeight, Timestamp: testCase.proposalTime, } @@ -272,7 +279,7 @@ func TestIsTimely(t *testing.T) { MessageDelay: testCase.msgDelay, } - ti := p.IsTimely(testCase.recvTime, sp) + ti := p.IsTimely(testCase.recvTime, sp, testCase.round) assert.Equal(t, testCase.expectTimely, ti) }) }