diff --git a/state/state.go b/state/state.go index 5862162d1..85d0f5612 100644 --- a/state/state.go +++ b/state/state.go @@ -304,6 +304,24 @@ func MedianTime(commit *types.Commit, validators *types.ValidatorSet) time.Time return weightedMedian(weightedTimes, totalVotingPower) } +// IsTimely validates that the passed in block timestamp is 'timely' according to the proposer-based timestamp algorithm. +// To evaluate if a timestamp is timely, it is compared to the local time of the validator along with the configured +// Precision and MsgDelay parameters. +// Specifically, a proposed block timestamp is considered timely if it is satisfies the following inequalities: +// +// proposedBlockTime < validatorLocalTime + Precision + MsgDelay && proposedBlockTime > validatorLocaltime - Precision. +// +// 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 IsTimely(bt time.Time, lt time.Time, precision time.Duration, msgDelay time.Duration) bool { + lhs := lt.Add(-precision).UnixMilli() + rhs := lt.Add(precision).Add(msgDelay).UnixMilli() + if lhs < bt.UnixMilli() && bt.UnixMilli() < rhs { + return true + } + return false +} + //------------------------------------------------------------------------ // Genesis diff --git a/state/time_test.go b/state/time_test.go index 893ade7ea..6ccb8cc91 100644 --- a/state/time_test.go +++ b/state/time_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" tmtime "github.com/tendermint/tendermint/libs/time" ) @@ -55,3 +56,53 @@ func TestWeightedMedian(t *testing.T) { assert.Equal(t, true, (median.After(t1) || median.Equal(t1)) && (median.Before(t4) || median.Equal(t4))) } + +func TestIsTimely(t *testing.T) { + genesisTime, err := time.Parse(time.RFC3339, "2019-03-13T23:00:00Z") + require.NoError(t, err) + testCases := []struct { + name string + blockTime time.Time + localTime time.Time + precision time.Duration + msgDelay time.Duration + expectTimely bool + }{ + { + // Checking that the following inequality evaluates to true: + // 1 - 2 < 0 < 1 + 2 + 1 + name: "basic timely", + blockTime: genesisTime, + localTime: genesisTime.Add(1 * time.Millisecond), + precision: time.Millisecond * 2, + msgDelay: time.Millisecond, + expectTimely: true, + }, + { + // Checking that the following inequality evaluates to false: + // 3 - 2 < 0 < 3 + 2 + 1 + name: "local time too large", + blockTime: genesisTime, + localTime: genesisTime.Add(3 * time.Millisecond), + precision: time.Millisecond * 2, + msgDelay: time.Millisecond, + expectTimely: false, + }, + { + // Checking that the following inequality evaluates to false: + // 0 - 2 < 2 < 2 + 1 + name: "block time too large", + blockTime: genesisTime.Add(4 * time.Millisecond), + localTime: genesisTime, + precision: time.Millisecond * 2, + msgDelay: time.Millisecond, + expectTimely: false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ti := IsTimely(testCase.blockTime, testCase.localTime, testCase.precision, testCase.msgDelay) + assert.Equal(t, testCase.expectTimely, ti) + }) + } +}