From 32c4827a8521c0d1965d3a5152c1cc6c608bb2ea Mon Sep 17 00:00:00 2001 From: Charly Date: Wed, 6 Jul 2022 14:31:27 +0200 Subject: [PATCH] chore: add GetTimestampAtHeight to client state #888 (#1659) Co-authored-by: Bo Du --- CHANGELOG.md | 1 + .../core/02-client/legacy/v100/solomachine.go | 7 +++ modules/core/exported/client.go | 6 +++ .../06-solomachine/types/client_state.go | 13 +++++ .../06-solomachine/types/client_state_test.go | 50 ++++++++++++++++++- .../07-tendermint/types/client_state.go | 15 ++++++ .../07-tendermint/types/client_state_test.go | 47 +++++++++++++++++ 7 files changed, 138 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a43567ede3..c021f77df94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,6 +121,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements +* (client) [\#888](https://github.com/cosmos/ibc-go/pull/888) Add `GetTimestampAtHeight` to `ClientState` * (interchain-accounts) [\#1037](https://github.com/cosmos/ibc-go/pull/1037) Add a function `InitModule` to the interchain accounts `AppModule`. This function should be called within the upgrade handler when adding the interchain accounts module to a chain. It should be called in place of InitGenesis (set the consensus version in the version map). * (testing) [\#1003](https://github.com/cosmos/ibc-go/pull/1003) Testing chain's `Signer` fields has changed from `[]tmtypes.PrivValidator` to `map[string]tmtypes.PrivValidator` to accomodate valset updates changing the order of the ValidatorSet. * (testing) [\#1003](https://github.com/cosmos/ibc-go/pull/1003) `SignAndDeliver` will now just deliver the transaction without creating and committing a block. Thus, it requires that `BeginBlock` MUST be called before `SignAndDeliver` diff --git a/modules/core/02-client/legacy/v100/solomachine.go b/modules/core/02-client/legacy/v100/solomachine.go index e264b354ca1..d41d6c31dbc 100644 --- a/modules/core/02-client/legacy/v100/solomachine.go +++ b/modules/core/02-client/legacy/v100/solomachine.go @@ -211,6 +211,13 @@ func (cs ClientState) VerifyNextSequenceRecv( panic("legacy solo machine is deprecated!") } +// GetTimestampAtHeight panics! +func (cs ClientState) GetTimestampAtHeight( + sdk.Context, sdk.KVStore, codec.BinaryCodec, exported.Height, +) (uint64, error) { + panic("legacy solo machine is deprecated!") +} + // VerifyMembership panics! func (cs *ClientState) VerifyMembership( ctx sdk.Context, diff --git a/modules/core/exported/client.go b/modules/core/exported/client.go index c319b7bd139..f71715aa6c5 100644 --- a/modules/core/exported/client.go +++ b/modules/core/exported/client.go @@ -214,6 +214,12 @@ type ClientState interface { channelID string, nextSequenceRecv uint64, ) error + GetTimestampAtHeight( + ctx sdk.Context, + clientStore sdk.KVStore, + cdc codec.BinaryCodec, + height Height, + ) (uint64, error) } // ConsensusState is the state of the consensus process diff --git a/modules/light-clients/06-solomachine/types/client_state.go b/modules/light-clients/06-solomachine/types/client_state.go index 9cd172925a1..06dbccb6e64 100644 --- a/modules/light-clients/06-solomachine/types/client_state.go +++ b/modules/light-clients/06-solomachine/types/client_state.go @@ -39,6 +39,19 @@ func (cs ClientState) GetLatestHeight() exported.Height { return clienttypes.NewHeight(0, cs.Sequence) } +// GetTimestampAtHeight returns the timestamp in nanoseconds of the consensus state at the given height. +func (cs ClientState) GetTimestampAtHeight( + _ sdk.Context, + clientStore sdk.KVStore, + cdc codec.BinaryCodec, + height exported.Height, +) (uint64, error) { + if !cs.GetLatestHeight().EQ(height) { + return 0, sdkerrors.Wrapf(ErrInvalidSequence, "not latest height (%s)", height) + } + return cs.ConsensusState.Timestamp, nil +} + // Status returns the status of the solo machine client. // The client may be: // - Active: if frozen sequence is 0 diff --git a/modules/light-clients/06-solomachine/types/client_state_test.go b/modules/light-clients/06-solomachine/types/client_state_test.go index 09ea9693119..3e0c4904b7b 100644 --- a/modules/light-clients/06-solomachine/types/client_state_test.go +++ b/modules/light-clients/06-solomachine/types/client_state_test.go @@ -27,7 +27,7 @@ var ( func (suite *SoloMachineTestSuite) TestStatus() { clientState := suite.solomachine.ClientState() - // solo machine discards arguements + // solo machine discards arguments status := clientState.Status(suite.chainA.GetContext(), nil, nil) suite.Require().Equal(exported.Active, status) @@ -845,3 +845,51 @@ func (suite *SoloMachineTestSuite) TestVerifyNextSeqRecv() { } } } + +func (suite *SoloMachineTestSuite) TestGetTimestampAtHeight() { + tmPath := ibctesting.NewPath(suite.chainA, suite.chainB) + suite.coordinator.SetupClients(tmPath) + // Single setup for all test cases. + suite.SetupTest() + + testCases := []struct { + name string + clientState *types.ClientState + height exported.Height + expValue uint64 + expPass bool + }{ + { + name: "get timestamp at height exists", + clientState: suite.solomachine.ClientState(), + height: suite.solomachine.ClientState().GetLatestHeight(), + expValue: suite.solomachine.ClientState().ConsensusState.Timestamp, + expPass: true, + }, + { + name: "get timestamp at height not exists", + clientState: suite.solomachine.ClientState(), + height: suite.solomachine.ClientState().GetLatestHeight().Increment(), + }, + } + + for i, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + ctx := suite.chainA.GetContext() + + ts, err := tc.clientState.GetTimestampAtHeight( + ctx, suite.store, suite.chainA.Codec, tc.height, + ) + + suite.Require().Equal(tc.expValue, ts) + + if tc.expPass { + suite.Require().NoError(err, "valid test case %d failed: %s", i, tc.name) + } else { + suite.Require().Error(err, "invalid test case %d passed: %s", i, tc.name) + } + }) + } +} diff --git a/modules/light-clients/07-tendermint/types/client_state.go b/modules/light-clients/07-tendermint/types/client_state.go index c78d54b3168..029f8da0932 100644 --- a/modules/light-clients/07-tendermint/types/client_state.go +++ b/modules/light-clients/07-tendermint/types/client_state.go @@ -58,6 +58,21 @@ func (cs ClientState) GetLatestHeight() exported.Height { return cs.LatestHeight } +// GetTimestampAtHeight returns the timestamp in nanoseconds of the consensus state at the given height. +func (cs ClientState) GetTimestampAtHeight( + ctx sdk.Context, + clientStore sdk.KVStore, + cdc codec.BinaryCodec, + height exported.Height, +) (uint64, error) { + // get consensus state at height from clientStore to check for expiry + consState, found := GetConsensusState(clientStore, cdc, height) + if !found { + return 0, sdkerrors.Wrapf(clienttypes.ErrConsensusStateNotFound, "height (%s)", height) + } + return consState.GetTimestamp(), nil +} + // Status returns the status of the tendermint client. // The client may be: // - Active: FrozenHeight is zero and client is not expired diff --git a/modules/light-clients/07-tendermint/types/client_state_test.go b/modules/light-clients/07-tendermint/types/client_state_test.go index b9d7c7f7016..0a46d6dd7f0 100644 --- a/modules/light-clients/07-tendermint/types/client_state_test.go +++ b/modules/light-clients/07-tendermint/types/client_state_test.go @@ -1390,3 +1390,50 @@ func (suite *TendermintTestSuite) TestVerifyNextSeqRecv() { }) } } + +func (suite *TendermintTestSuite) TestGetTimestampAtHeight() { + suite.SetupTest() + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + path.SetChannelOrdered() + suite.coordinator.Setup(path) + + ctx := suite.chainA.GetContext() + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, path.EndpointA.ClientID) + clientState := path.EndpointA.GetClientState() + + testCases := []struct { + name string + height exported.Height + expValue uint64 + expPass bool + }{ + { + name: "get timestamp at height exists", + height: clientState.GetLatestHeight(), + expValue: path.EndpointA.GetConsensusState(clientState.GetLatestHeight()).GetTimestamp(), + expPass: true, + }, + { + name: "get timestamp at height not exists", + height: clientState.GetLatestHeight().Increment(), + }, + } + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + ts, err := clientState.GetTimestampAtHeight( + ctx, clientStore, suite.chainA.Codec, tc.height, + ) + + suite.Require().Equal(tc.expValue, ts) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +}