From f03da27f19b01051ff2d689b1da29ad73552460d Mon Sep 17 00:00:00 2001 From: William Banfield Date: Thu, 28 Oct 2021 11:29:30 +0200 Subject: [PATCH] move time checks into block.go and add time source mechanism --- libs/time/mocks/source.go | 28 +++++++++++++++++ libs/time/time.go | 14 +++++++++ state/time_test.go | 51 ------------------------------ types/block.go | 20 ++++++++++++ types/block_test.go | 65 +++++++++++++++++++++++++++++++++++++++ types/params.go | 6 ++++ 6 files changed, 133 insertions(+), 51 deletions(-) create mode 100644 libs/time/mocks/source.go diff --git a/libs/time/mocks/source.go b/libs/time/mocks/source.go new file mode 100644 index 000000000..a8e49b314 --- /dev/null +++ b/libs/time/mocks/source.go @@ -0,0 +1,28 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + time "time" + + mock "github.com/stretchr/testify/mock" +) + +// Source is an autogenerated mock type for the Source type +type Source struct { + mock.Mock +} + +// Now provides a mock function with given fields: +func (_m *Source) Now() time.Time { + ret := _m.Called() + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} diff --git a/libs/time/time.go b/libs/time/time.go index 786f9bbb4..7ab45d8f1 100644 --- a/libs/time/time.go +++ b/libs/time/time.go @@ -15,3 +15,17 @@ func Now() time.Time { func Canonical(t time.Time) time.Time { return t.Round(0).UTC() } + +//go:generate ../../scripts/mockery_generate.sh Source + +// Source is an interface that defines a way to fetch the current time. +type Source interface { + Now() time.Time +} + +// DefaultSource implements the Source interface using the system clock provided by the standard library. +type DefaultSource struct{} + +func (DefaultSource) Now() time.Time { + return Now() +} diff --git a/state/time_test.go b/state/time_test.go index 6ccb8cc91..893ade7ea 100644 --- a/state/time_test.go +++ b/state/time_test.go @@ -5,7 +5,6 @@ import ( "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" tmtime "github.com/tendermint/tendermint/libs/time" ) @@ -56,53 +55,3 @@ 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) - }) - } -} diff --git a/types/block.go b/types/block.go index dcae746a3..780c139ad 100644 --- a/types/block.go +++ b/types/block.go @@ -17,6 +17,7 @@ import ( "github.com/tendermint/tendermint/libs/bits" tmbytes "github.com/tendermint/tendermint/libs/bytes" tmmath "github.com/tendermint/tendermint/libs/math" + tmtime "github.com/tendermint/tendermint/libs/time" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" "github.com/tendermint/tendermint/version" ) @@ -94,6 +95,25 @@ func (b *Block) ValidateBasic() error { return nil } +// IsTimely validates that the block timestamp is 'timely' according to the proposer-based timestamp algorithm. +// To evaluate if a block is timely, its timestamp 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 (b *Block) IsTimely(clock tmtime.Source, p TimestampParams) bool { + lt := clock.Now() + lhs := lt.Add(-p.Precision).UnixMilli() + rhs := lt.Add(p.Precision).Add(p.MsgDelay).UnixMilli() + if lhs < b.Header.Time.UnixMilli() && b.Header.Time.UnixMilli() < rhs { + return true + } + return false +} + // fillHeader fills in any remaining header fields that are a function of the block data func (b *Block) fillHeader() { if b.LastCommitHash == nil { diff --git a/types/block_test.go b/types/block_test.go index 1c762653b..fa0f417b5 100644 --- a/types/block_test.go +++ b/types/block_test.go @@ -24,6 +24,7 @@ import ( "github.com/tendermint/tendermint/libs/bytes" tmrand "github.com/tendermint/tendermint/libs/rand" tmtime "github.com/tendermint/tendermint/libs/time" + tmtimemocks "github.com/tendermint/tendermint/libs/time/mocks" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmversion "github.com/tendermint/tendermint/proto/tendermint/version" "github.com/tendermint/tendermint/version" @@ -1347,3 +1348,67 @@ func TestHeaderHashVector(t *testing.T) { require.Equal(t, tc.expBytes, hex.EncodeToString(hash)) } } + +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) { + b := Block{ + Header: Header{ + Time: testCase.blockTime, + }, + } + + p := TimestampParams{ + Precision: testCase.precision, + MsgDelay: testCase.msgDelay, + } + + mockSource := new(tmtimemocks.Source) + mockSource.On("Now").Return(testCase.localTime) + + ti := b.IsTimely(mockSource, p) + assert.Equal(t, testCase.expectTimely, ti) + }) + } +} diff --git a/types/params.go b/types/params.go index 32d0f71c8..f0ca905f7 100644 --- a/types/params.go +++ b/types/params.go @@ -75,6 +75,12 @@ type VersionParams struct { AppVersion uint64 `json:"app_version"` } +type TimestampParams struct { + Precision time.Duration `json:"precision"` + Accuracy time.Duration `json:"accuracy"` + MsgDelay time.Duration `json:"msg_delay"` +} + // DefaultConsensusParams returns a default ConsensusParams. func DefaultConsensusParams() *ConsensusParams { return &ConsensusParams{