From e70f6762dc3d1d589acb1b1fbfe96da2e8da6850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadej=20Jane=C5=BE?= Date: Thu, 5 Aug 2021 22:15:07 +0200 Subject: [PATCH] go/roothash/api/block: Use custom Timestamp type for block's header This enables prettier Oasis Node's control status CLI command's output for runtimes' latest_time field and matches the format of consensus' latest_time field. --- .changelog/4183.feature.md | 5 ++ go/control/api/api.go | 3 +- go/roothash/api/api.go | 2 +- go/roothash/api/block/block.go | 4 +- go/roothash/api/block/header.go | 25 +++++++++- go/roothash/api/block/header_test.go | 68 ++++++++++++++++++++++++++++ go/roothash/tests/tester.go | 2 +- 7 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 .changelog/4183.feature.md diff --git a/.changelog/4183.feature.md b/.changelog/4183.feature.md new file mode 100644 index 00000000000..63261a5c986 --- /dev/null +++ b/.changelog/4183.feature.md @@ -0,0 +1,5 @@ +go/roothash/api/block: Use custom `Timestamp` type for block's header + +This enables prettier Oasis Node's `control status` CLI command's output +for runtimes' `latest_time` field and matches the format of consensus' +`latest_time` field. diff --git a/go/control/api/api.go b/go/control/api/api.go index ccc398ed681..681d0933340 100644 --- a/go/control/api/api.go +++ b/go/control/api/api.go @@ -14,6 +14,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/node" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" registry "github.com/oasisprotocol/oasis-core/go/registry/api" + block "github.com/oasisprotocol/oasis-core/go/roothash/api/block" storage "github.com/oasisprotocol/oasis-core/go/storage/api" upgrade "github.com/oasisprotocol/oasis-core/go/upgrade/api" commonWorker "github.com/oasisprotocol/oasis-core/go/worker/common/api" @@ -113,7 +114,7 @@ type RuntimeStatus struct { // LatestHash is the hash of the latest runtime block. LatestHash hash.Hash `json:"latest_hash"` // LatestTime is the timestamp of the latest runtime block. - LatestTime uint64 `json:"latest_time"` + LatestTime block.Timestamp `json:"latest_time"` // LatestStateRoot is the Merkle root of the runtime state tree. LatestStateRoot storage.Root `json:"latest_state_root"` diff --git a/go/roothash/api/api.go b/go/roothash/api/api.go index 1dceec277b7..85c9a5c1c5f 100644 --- a/go/roothash/api/api.go +++ b/go/roothash/api/api.go @@ -462,7 +462,7 @@ func SanityCheckBlocks(blocks map[common.Namespace]*block.Block) error { for _, blk := range blocks { hdr := blk.Header - if hdr.Timestamp > uint64(time.Now().Unix()+61*60) { + if hdr.Timestamp > block.Timestamp(time.Now().Unix()+61*60) { return fmt.Errorf("roothash: sanity check failed: block header timestamp is more than 1h1m in the future") } } diff --git a/go/roothash/api/block/block.go b/go/roothash/api/block/block.go index 612fda734d6..00cad043a8f 100644 --- a/go/roothash/api/block/block.go +++ b/go/roothash/api/block/block.go @@ -17,7 +17,7 @@ func NewGenesisBlock(id common.Namespace, timestamp uint64) *Block { var blk Block blk.Header.Version = 0 - blk.Header.Timestamp = timestamp + blk.Header.Timestamp = Timestamp(timestamp) blk.Header.HeaderType = Normal blk.Header.Namespace = id blk.Header.PreviousHash.Empty() @@ -35,7 +35,7 @@ func NewEmptyBlock(child *Block, timestamp uint64, htype HeaderType) *Block { blk.Header.Version = child.Header.Version blk.Header.Namespace = child.Header.Namespace blk.Header.Round = child.Header.Round + 1 - blk.Header.Timestamp = timestamp + blk.Header.Timestamp = Timestamp(timestamp) blk.Header.HeaderType = htype blk.Header.PreviousHash = child.Header.EncodedHash() blk.Header.IORoot.Empty() diff --git a/go/roothash/api/block/header.go b/go/roothash/api/block/header.go index 62355934d91..25195918393 100644 --- a/go/roothash/api/block/header.go +++ b/go/roothash/api/block/header.go @@ -3,6 +3,7 @@ package block import ( "bytes" "errors" + "time" "github.com/oasisprotocol/oasis-core/go/common" "github.com/oasisprotocol/oasis-core/go/common/cbor" @@ -17,6 +18,28 @@ var ErrInvalidVersion = errors.New("roothash: invalid version") // HeaderType is the type of header. type HeaderType uint8 +// Timestamp is a custom time stamp type that encodes like time.Time when +// marshaling to text. +type Timestamp uint64 + +// MarshalText encodes a Timestamp to text by converting it from Unix time to +// local time. +func (ts Timestamp) MarshalText() ([]byte, error) { + t := time.Unix(int64(ts), 0) + return t.MarshalText() +} + +// UnmarshalText decodes a text slice into a Timestamp. +func (ts *Timestamp) UnmarshalText(data []byte) error { + var t time.Time + err := t.UnmarshalText(data) + if err != nil { + return err + } + *ts = Timestamp(t.Unix()) + return nil +} + const ( // Invalid is an invalid header type and should never be stored. Invalid HeaderType = 0 @@ -57,7 +80,7 @@ type Header struct { // nolint: maligned Round uint64 `json:"round"` // Timestamp is the block timestamp (POSIX time). - Timestamp uint64 `json:"timestamp"` + Timestamp Timestamp `json:"timestamp"` // HeaderType is the header type. HeaderType HeaderType `json:"header_type"` diff --git a/go/roothash/api/block/header_test.go b/go/roothash/api/block/header_test.go index c5fd2074a4a..d0b95a1d4bf 100644 --- a/go/roothash/api/block/header_test.go +++ b/go/roothash/api/block/header_test.go @@ -3,6 +3,7 @@ package block import ( "math/big" "testing" + "time" "github.com/stretchr/testify/require" @@ -114,3 +115,70 @@ func TestVerifyStorageReceipt(t *testing.T) { err = header.VerifyStorageReceipt(&receipt) require.NoError(t, err, "correct receipt") } + +func TestTimestamp(t *testing.T) { + require := require.New(t) + + // Set local time zone to a fixed value to be able to compare the + // marshaled time stamps across different systems and configurations. + loc, err := time.LoadLocation("Pacific/Honolulu") + require.NoErrorf(err, "Failed to load a fixed time zone") + time.Local = loc + + testVectors := []struct { + timestamp Timestamp + timestampString string + timestampStringValid bool + timestampStringMatching bool + errMsg string + }{ + // Valid. + {1, "1969-12-31T14:00:01-10:00", true, true, ""}, + {1629075845, "2021-08-15T15:04:05-10:00", true, true, ""}, + {4772384038, "2121-03-25T12:13:58-10:00", true, true, ""}, + + // Invalid - wrong syntax for marshalled time stamps. + {1629075845, "2021-08-15T15:04:05Z-10:00", false, false, "parsing time \"2021-08-15T15:04:05Z-10:00\": extra text: \"-10:00\""}, + {1629032645, "2021-08-15T15:04:05+2:00", false, false, "parsing time \"2021-08-15T15:04:05+2:00\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"+2:00\" as \"Z07:00\""}, + + // Invalid - not marshaled using the correct time zone. + {1629039845, "2021-08-15T15:04:05Z", true, false, ""}, + {1629032645, "2021-08-15T15:04:05+02:00", true, false, ""}, + } + + for _, v := range testVectors { + var unmarshaledTimestamp Timestamp + err := unmarshaledTimestamp.UnmarshalText([]byte(v.timestampString)) + if !v.timestampStringValid { + require.EqualErrorf( + err, + v.errMsg, + "Unmarshaling invalid time stamp: '%s' should fail with expected error message", + v.timestampString, + ) + } else { + require.NoErrorf(err, "Failed to unmarshal a valid time stamp: '%s'", v.timestampString) + require.Equalf( + v.timestamp, + unmarshaledTimestamp, + "Unmarshaled time stamp doesn't equal expected time stamp: %s %#s", v.timestamp, unmarshaledTimestamp, + ) + } + + textTimestamp, err := v.timestamp.MarshalText() + require.NoError(err, "Failed to marshal a valid time stamp: '%s'", v.timestamp) + if v.timestampStringMatching { + require.Equal( + v.timestampString, + string(textTimestamp), + "Marshaled time stamp doesn't equal expected text time stamp", + ) + } else { + require.NotEqual( + v.timestampString, + string(textTimestamp), + "Marshaled time stamp shouldn't equal the expected text time stamp for invalid test cases", + ) + } + } +} diff --git a/go/roothash/tests/tester.go b/go/roothash/tests/tester.go index e34cf594525..a25a9a4b75b 100644 --- a/go/roothash/tests/tester.go +++ b/go/roothash/tests/tester.go @@ -312,7 +312,7 @@ func (s *runtimeState) generateExecutorCommitments(t *testing.T, consensus conse Version: 0, Namespace: child.Header.Namespace, Round: child.Header.Round + 1, - Timestamp: uint64(time.Now().Unix()), + Timestamp: block.Timestamp(time.Now().Unix()), HeaderType: block.Normal, PreviousHash: child.Header.EncodedHash(), IORoot: ioRootHash,