diff --git a/CHANGELOG.md b/CHANGELOG.md index c302b4c759e..663b11f848b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,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 c9814439902..5d53152d599 100644 --- a/modules/core/02-client/legacy/v100/solomachine.go +++ b/modules/core/02-client/legacy/v100/solomachine.go @@ -187,6 +187,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!") +} + // ClientType panics! func (ConsensusState) ClientType() string { panic("legacy solo machine is deprecated!") diff --git a/modules/core/exported/client.go b/modules/core/exported/client.go index 39095aff28f..2a5946d1977 100644 --- a/modules/core/exported/client.go +++ b/modules/core/exported/client.go @@ -177,6 +177,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 ef3088b314f..cca6f90ca2f 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().Increment().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 51f826979fd..0a4b3d2a1a9 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, err := GetConsensusState(clientStore, cdc, height) + if err != nil { + return 0, sdkerrors.Wrapf(err, "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 cf52d2996b5..b2538b18655 100644 --- a/modules/light-clients/07-tendermint/types/client_state_test.go +++ b/modules/light-clients/07-tendermint/types/client_state_test.go @@ -881,3 +881,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) + } + }) + } +} diff --git a/modules/light-clients/09-localhost/types/client_state.go b/modules/light-clients/09-localhost/types/client_state.go index 728e4ec5f12..cf35878f218 100644 --- a/modules/light-clients/09-localhost/types/client_state.go +++ b/modules/light-clients/09-localhost/types/client_state.go @@ -42,6 +42,16 @@ func (cs ClientState) GetLatestHeight() exported.Height { return cs.Height } +// GetTimestampAtHeight returns 0. Localhost client has no consensus state. +func (cs ClientState) GetTimestampAtHeight( + _ sdk.Context, + _ sdk.KVStore, + _ codec.BinaryCodec, + _ exported.Height, +) (uint64, error) { + return 0, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "localhost client has no consensus state") +} + // Status always returns Active. The localhost status cannot be changed. func (cs ClientState) Status(_ sdk.Context, _ sdk.KVStore, _ codec.BinaryCodec, ) exported.Status { diff --git a/modules/light-clients/09-localhost/types/client_state_test.go b/modules/light-clients/09-localhost/types/client_state_test.go index a54cc8efe9a..70d15c0f1fb 100644 --- a/modules/light-clients/09-localhost/types/client_state_test.go +++ b/modules/light-clients/09-localhost/types/client_state_test.go @@ -527,3 +527,42 @@ func (suite *LocalhostTestSuite) TestVerifyNextSeqRecv() { }) } } + +func (suite *LocalhostTestSuite) TestGetTimestampAtHeight() { + testCases := []struct { + name string + clientState *types.ClientState + malleate func() + checkHeight exported.Height + }{ + { + name: "get timestamp at height returns error", + clientState: types.NewClientState("chainID", clientHeight), + checkHeight: clientHeight, + }, + { + name: "get timestamp at client height + 1 returns error", + clientState: types.NewClientState("chainID", clientHeight), + checkHeight: clientHeight.Increment(), + }, + { + name: "get timestamp at client height + 2 returns error", + clientState: types.NewClientState("chainID", clientHeight), + checkHeight: clientHeight.Increment().Increment(), + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + + _, err := tc.clientState.GetTimestampAtHeight( + suite.ctx, suite.store, suite.cdc, clientHeight, + ) + + suite.Require().Error(err) + }) + } +}