diff --git a/modules/light-clients/07-tendermint/types/client_state.go b/modules/light-clients/07-tendermint/types/client_state.go index f0d0e79972f..404ed692fe5 100644 --- a/modules/light-clients/07-tendermint/types/client_state.go +++ b/modules/light-clients/07-tendermint/types/client_state.go @@ -192,6 +192,7 @@ func (cs ClientState) Initialize(ctx sdk.Context, _ codec.BinaryCodec, clientSto // VerifyClientState verifies a proof of the client state of the running chain // stored on the target machine +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (cs ClientState) VerifyClientState( store sdk.KVStore, cdc codec.BinaryCodec, @@ -231,6 +232,7 @@ func (cs ClientState) VerifyClientState( // VerifyClientConsensusState verifies a proof of the consensus state of the // Tendermint client stored on the target machine. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (cs ClientState) VerifyClientConsensusState( store sdk.KVStore, cdc codec.BinaryCodec, @@ -275,6 +277,7 @@ func (cs ClientState) VerifyClientConsensusState( // VerifyConnectionState verifies a proof of the connection state of the // specified connection end stored on the target machine. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (cs ClientState) VerifyConnectionState( store sdk.KVStore, cdc codec.BinaryCodec, @@ -314,6 +317,7 @@ func (cs ClientState) VerifyConnectionState( // VerifyChannelState verifies a proof of the channel state of the specified // channel end, under the specified port, stored on the target machine. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (cs ClientState) VerifyChannelState( store sdk.KVStore, cdc codec.BinaryCodec, @@ -354,6 +358,7 @@ func (cs ClientState) VerifyChannelState( // VerifyPacketCommitment verifies a proof of an outgoing packet commitment at // the specified port, specified channel, and specified sequence. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (cs ClientState) VerifyPacketCommitment( ctx sdk.Context, store sdk.KVStore, @@ -393,6 +398,7 @@ func (cs ClientState) VerifyPacketCommitment( // VerifyPacketAcknowledgement verifies a proof of an incoming packet // acknowledgement at the specified port, specified channel, and specified sequence. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (cs ClientState) VerifyPacketAcknowledgement( ctx sdk.Context, store sdk.KVStore, @@ -433,6 +439,7 @@ func (cs ClientState) VerifyPacketAcknowledgement( // VerifyPacketReceiptAbsence verifies a proof of the absence of an // incoming packet receipt at the specified port, specified channel, and // specified sequence. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (cs ClientState) VerifyPacketReceiptAbsence( ctx sdk.Context, store sdk.KVStore, @@ -471,6 +478,7 @@ func (cs ClientState) VerifyPacketReceiptAbsence( // VerifyNextSequenceRecv verifies a proof of the next sequence number to be // received of the specified channel at the specified port. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (cs ClientState) VerifyNextSequenceRecv( ctx sdk.Context, store sdk.KVStore, @@ -509,39 +517,97 @@ func (cs ClientState) VerifyNextSequenceRecv( return nil } +// VerifyMembership is a generic proof verification method which verifies a proof of the existence of a value at a given CommitmentPath at the specified height. +// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24). +func (cs ClientState) VerifyMembership( + ctx sdk.Context, + clientStore sdk.KVStore, + cdc codec.BinaryCodec, + height exported.Height, + delayTimePeriod uint64, + delayBlockPeriod uint64, + proof []byte, + path []byte, + value []byte, +) error { + if cs.GetLatestHeight().LT(height) { + return sdkerrors.Wrapf( + sdkerrors.ErrInvalidHeight, + "client state height < proof height (%d < %d), please ensure the client has been updated", cs.GetLatestHeight(), height, + ) + } + + if err := verifyDelayPeriodPassed(ctx, clientStore, height, delayTimePeriod, delayBlockPeriod); err != nil { + return err + } + + var merkleProof commitmenttypes.MerkleProof + if err := cdc.Unmarshal(proof, &merkleProof); err != nil { + return sdkerrors.Wrap(commitmenttypes.ErrInvalidProof, "failed to unmarshal proof into ICS 23 commitment merkle proof") + } + + var merklePath commitmenttypes.MerklePath + if err := cdc.Unmarshal(path, &merklePath); err != nil { + return sdkerrors.Wrap(commitmenttypes.ErrInvalidProof, "failed to unmarshal path into ICS 23 commitment merkle path") + } + + consensusState, found := GetConsensusState(clientStore, cdc, height) + if !found { + return sdkerrors.Wrap(clienttypes.ErrConsensusStateNotFound, "please ensure the proof was constructed against a height that exists on the client") + } + + if err := merkleProof.VerifyMembership(cs.ProofSpecs, consensusState.GetRoot(), merklePath, value); err != nil { + return err + } + + return nil +} + // verifyDelayPeriodPassed will ensure that at least delayTimePeriod amount of time and delayBlockPeriod number of blocks have passed // since consensus state was submitted before allowing verification to continue. func verifyDelayPeriodPassed(ctx sdk.Context, store sdk.KVStore, proofHeight exported.Height, delayTimePeriod, delayBlockPeriod uint64) error { - // check that executing chain's timestamp has passed consensusState's processed time + delay time period - processedTime, ok := GetProcessedTime(store, proofHeight) - if !ok { - return sdkerrors.Wrapf(ErrProcessedTimeNotFound, "processed time not found for height: %s", proofHeight) - } - currentTimestamp := uint64(ctx.BlockTime().UnixNano()) - validTime := processedTime + delayTimePeriod - // NOTE: delay time period is inclusive, so if currentTimestamp is validTime, then we return no error - if currentTimestamp < validTime { - return sdkerrors.Wrapf(ErrDelayPeriodNotPassed, "cannot verify packet until time: %d, current time: %d", - validTime, currentTimestamp) - } - // check that executing chain's height has passed consensusState's processed height + delay block period - processedHeight, ok := GetProcessedHeight(store, proofHeight) - if !ok { - return sdkerrors.Wrapf(ErrProcessedHeightNotFound, "processed height not found for height: %s", proofHeight) + if delayTimePeriod != 0 { + // check that executing chain's timestamp has passed consensusState's processed time + delay time period + processedTime, ok := GetProcessedTime(store, proofHeight) + if !ok { + return sdkerrors.Wrapf(ErrProcessedTimeNotFound, "processed time not found for height: %s", proofHeight) + } + + currentTimestamp := uint64(ctx.BlockTime().UnixNano()) + validTime := processedTime + delayTimePeriod + + // NOTE: delay time period is inclusive, so if currentTimestamp is validTime, then we return no error + if currentTimestamp < validTime { + return sdkerrors.Wrapf(ErrDelayPeriodNotPassed, "cannot verify packet until time: %d, current time: %d", + validTime, currentTimestamp) + } + } - currentHeight := clienttypes.GetSelfHeight(ctx) - validHeight := clienttypes.NewHeight(processedHeight.GetRevisionNumber(), processedHeight.GetRevisionHeight()+delayBlockPeriod) - // NOTE: delay block period is inclusive, so if currentHeight is validHeight, then we return no error - if currentHeight.LT(validHeight) { - return sdkerrors.Wrapf(ErrDelayPeriodNotPassed, "cannot verify packet until height: %s, current height: %s", - validHeight, currentHeight) + + if delayBlockPeriod != 0 { + // check that executing chain's height has passed consensusState's processed height + delay block period + processedHeight, ok := GetProcessedHeight(store, proofHeight) + if !ok { + return sdkerrors.Wrapf(ErrProcessedHeightNotFound, "processed height not found for height: %s", proofHeight) + } + + currentHeight := clienttypes.GetSelfHeight(ctx) + validHeight := clienttypes.NewHeight(processedHeight.GetRevisionNumber(), processedHeight.GetRevisionHeight()+delayBlockPeriod) + + // NOTE: delay block period is inclusive, so if currentHeight is validHeight, then we return no error + if currentHeight.LT(validHeight) { + return sdkerrors.Wrapf(ErrDelayPeriodNotPassed, "cannot verify packet until height: %s, current height: %s", + validHeight, currentHeight) + } } + return nil } // produceVerificationArgs perfoms the basic checks on the arguments that are // shared between the verification functions and returns the unmarshalled // merkle proof, the consensus state and an error if one occurred. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func produceVerificationArgs( store sdk.KVStore, cdc codec.BinaryCodec, 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..58416f29832 100644 --- a/modules/light-clients/07-tendermint/types/client_state_test.go +++ b/modules/light-clients/07-tendermint/types/client_state_test.go @@ -4,7 +4,9 @@ import ( "time" ics23 "github.com/confio/ics23/go" + sdk "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" commitmenttypes "github.com/cosmos/ibc-go/v3/modules/core/23-commitment/types" @@ -201,6 +203,277 @@ func (suite *TendermintTestSuite) TestInitialize() { } } +func (suite *TendermintTestSuite) TestVerifyMembership() { + var ( + testingpath *ibctesting.Path + delayTimePeriod uint64 + delayBlockPeriod uint64 + proofHeight exported.Height + proof []byte + path []byte + value []byte + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "successful ClientState verification", + func() { + // default proof construction uses ClientState + }, + true, + }, + { + "successful ConsensusState verification", func() { + key := host.FullConsensusStateKey(testingpath.EndpointB.ClientID, testingpath.EndpointB.GetClientState().GetLatestHeight()) + merklePath := commitmenttypes.NewMerklePath(string(key)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chainB.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path, err = suite.chainB.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + proof, proofHeight = suite.chainB.QueryProof(key) + + consensusState := testingpath.EndpointB.GetConsensusState(testingpath.EndpointB.GetClientState().GetLatestHeight()).(*types.ConsensusState) + value, err = suite.chainB.Codec.MarshalInterface(consensusState) + suite.Require().NoError(err) + }, + true, + }, + { + "successful Connection verification", func() { + key := host.ConnectionKey(testingpath.EndpointB.ConnectionID) + merklePath := commitmenttypes.NewMerklePath(string(key)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chainB.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path, err = suite.chainB.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + proof, proofHeight = suite.chainB.QueryProof(key) + + connection := testingpath.EndpointB.GetConnection() + value, err = suite.chainB.Codec.Marshal(&connection) + suite.Require().NoError(err) + }, + true, + }, + { + "successful Channel verification", func() { + key := host.ChannelKey(testingpath.EndpointB.ChannelConfig.PortID, testingpath.EndpointB.ChannelID) + merklePath := commitmenttypes.NewMerklePath(string(key)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chainB.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path, err = suite.chainB.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + proof, proofHeight = suite.chainB.QueryProof(key) + + channel := testingpath.EndpointB.GetChannel() + value, err = suite.chainB.Codec.Marshal(&channel) + suite.Require().NoError(err) + }, + true, + }, + { + "successful PacketCommitment verification", func() { + // send from chainB to chainA since we are proving chainB sent a packet + packet := channeltypes.NewPacket(ibctesting.MockPacketData, 1, testingpath.EndpointB.ChannelConfig.PortID, testingpath.EndpointB.ChannelID, testingpath.EndpointA.ChannelConfig.PortID, testingpath.EndpointA.ChannelID, clienttypes.NewHeight(0, 100), 0) + err := testingpath.EndpointB.SendPacket(packet) + suite.Require().NoError(err) + + // make packet commitment proof + key := host.PacketCommitmentKey(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) + merklePath := commitmenttypes.NewMerklePath(string(key)) + merklePath, err = commitmenttypes.ApplyPrefix(suite.chainB.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path, err = suite.chainB.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + proof, proofHeight = testingpath.EndpointB.QueryProof(key) + + value = channeltypes.CommitPacket(suite.chainA.App.GetIBCKeeper().Codec(), packet) + }, true, + }, + { + "successful Acknowledgement verification", func() { + // send from chainA to chainB since we are proving chainB wrote an acknowledgement + packet := channeltypes.NewPacket(ibctesting.MockPacketData, 1, testingpath.EndpointA.ChannelConfig.PortID, testingpath.EndpointA.ChannelID, testingpath.EndpointB.ChannelConfig.PortID, testingpath.EndpointB.ChannelID, clienttypes.NewHeight(0, 100), 0) + err := testingpath.EndpointA.SendPacket(packet) + suite.Require().NoError(err) + + // write receipt and ack + err = testingpath.EndpointB.RecvPacket(packet) + suite.Require().NoError(err) + + key := host.PacketAcknowledgementKey(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) + merklePath := commitmenttypes.NewMerklePath(string(key)) + merklePath, err = commitmenttypes.ApplyPrefix(suite.chainB.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path, err = suite.chainB.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + proof, proofHeight = testingpath.EndpointB.QueryProof(key) + + value = channeltypes.CommitAcknowledgement(ibcmock.MockAcknowledgement.Acknowledgement()) + }, + true, + }, + { + "successful NextSequenceRecv verification", func() { + // send from chainA to chainB since we are proving chainB incremented the sequence recv + packet := channeltypes.NewPacket(ibctesting.MockPacketData, 1, testingpath.EndpointA.ChannelConfig.PortID, testingpath.EndpointA.ChannelID, testingpath.EndpointB.ChannelConfig.PortID, testingpath.EndpointB.ChannelID, clienttypes.NewHeight(0, 100), 0) + + // send packet + err := testingpath.EndpointA.SendPacket(packet) + suite.Require().NoError(err) + + // next seq recv incremented + err = testingpath.EndpointB.RecvPacket(packet) + suite.Require().NoError(err) + + key := host.NextSequenceRecvKey(packet.GetSourcePort(), packet.GetSourceChannel()) + merklePath := commitmenttypes.NewMerklePath(string(key)) + merklePath, err = commitmenttypes.ApplyPrefix(suite.chainB.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path, err = suite.chainB.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + proof, proofHeight = testingpath.EndpointB.QueryProof(key) + + value = sdk.Uint64ToBigEndian(packet.GetSequence() + 1) + }, + true, + }, + { + "successful verification outside IBC store", func() { + key := transfertypes.PortKey + merklePath := commitmenttypes.NewMerklePath(string(key)) + merklePath, err := commitmenttypes.ApplyPrefix(commitmenttypes.NewMerklePrefix([]byte(transfertypes.StoreKey)), merklePath) + suite.Require().NoError(err) + + path, err = suite.chainB.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + clientState := testingpath.EndpointA.GetClientState() + proof, proofHeight = suite.chainB.QueryProofForStore(transfertypes.StoreKey, key, int64(clientState.GetLatestHeight().GetRevisionHeight())) + + value = []byte(suite.chainB.GetSimApp().TransferKeeper.GetPort(suite.chainB.GetContext())) + suite.Require().NoError(err) + }, + true, + }, + { + "delay time period has passed", func() { + delayTimePeriod = uint64(time.Second.Nanoseconds()) + }, + true, + }, + { + "delay time period has not passed", func() { + delayTimePeriod = uint64(time.Hour.Nanoseconds()) + }, + false, + }, + { + "delay block period has passed", func() { + delayBlockPeriod = 1 + }, + true, + }, + { + "delay block period has not passed", func() { + delayBlockPeriod = 1000 + }, + false, + }, + { + "latest client height < height", func() { + proofHeight = testingpath.EndpointA.GetClientState().GetLatestHeight().Increment() + }, false, + }, + { + "failed to unmarshal merkle path", func() { + path = []byte("invalid merkle path") + }, false, + }, + { + "failed to unmarshal merkle proof", func() { + proof = invalidProof + }, false, + }, + { + "consensus state not found", func() { + proofHeight = clienttypes.ZeroHeight() + }, false, + }, + { + "proof verification failed", func() { + // change the value being prooved + value = []byte("invalid value") + }, false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() // reset + testingpath = ibctesting.NewPath(suite.chainA, suite.chainB) + testingpath.SetChannelOrdered() + suite.coordinator.Setup(testingpath) + + // reset time and block delays to 0, malleate may change to a specific non-zero value. + delayTimePeriod = 0 + delayBlockPeriod = 0 + + // create default proof, merklePath, and value which passes + // may be overwritten by malleate() + key := host.FullClientStateKey(testingpath.EndpointB.ClientID) + merklePath := commitmenttypes.NewMerklePath(string(key)) + merklePath, err := commitmenttypes.ApplyPrefix(suite.chainB.GetPrefix(), merklePath) + suite.Require().NoError(err) + + path, err = suite.chainA.Codec.Marshal(&merklePath) + suite.Require().NoError(err) + + proof, proofHeight = suite.chainB.QueryProof(key) + + clientState := testingpath.EndpointB.GetClientState().(*types.ClientState) + value, err = suite.chainB.Codec.MarshalInterface(clientState) + suite.Require().NoError(err) + + tc.malleate() // make changes as necessary + + clientState = testingpath.EndpointA.GetClientState().(*types.ClientState) + + ctx := suite.chainA.GetContext() + store := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, testingpath.EndpointA.ClientID) + + err = clientState.VerifyMembership( + ctx, store, suite.chainA.Codec, proofHeight, delayTimePeriod, delayBlockPeriod, + proof, path, value, + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (suite *TendermintTestSuite) TestVerifyClientConsensusState() { testCases := []struct { name string @@ -268,6 +541,7 @@ func (suite *TendermintTestSuite) TestVerifyClientConsensusState() { // test verification of the connection on chainB being represented in the // light client on chainA +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (suite *TendermintTestSuite) TestVerifyConnectionState() { var ( clientState *types.ClientState @@ -342,6 +616,7 @@ func (suite *TendermintTestSuite) TestVerifyConnectionState() { // test verification of the channel on chainB being represented in the light // client on chainA +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (suite *TendermintTestSuite) TestVerifyChannelState() { var ( clientState *types.ClientState @@ -417,6 +692,7 @@ func (suite *TendermintTestSuite) TestVerifyChannelState() { // test verification of the packet commitment on chainB being represented // in the light client on chainA. A send from chainB to chainA is simulated. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (suite *TendermintTestSuite) TestVerifyPacketCommitment() { var ( clientState *types.ClientState @@ -531,6 +807,7 @@ func (suite *TendermintTestSuite) TestVerifyPacketCommitment() { // test verification of the acknowledgement on chainB being represented // in the light client on chainA. A send and ack from chainA to chainB // is simulated. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (suite *TendermintTestSuite) TestVerifyPacketAcknowledgement() { var ( clientState *types.ClientState @@ -577,7 +854,6 @@ func (suite *TendermintTestSuite) TestVerifyPacketAcknowledgement() { }, expPass: false, }, - { "ApplyPrefix failed", func() { prefix = commitmenttypes.MerklePrefix{} @@ -765,6 +1041,7 @@ func (suite *TendermintTestSuite) TestVerifyPacketReceiptAbsence() { // test verification of the next receive sequence on chainB being represented // in the light client on chainA. A send and receive from chainB to chainA is // simulated. +// TODO: remove, https://github.com/cosmos/ibc-go/issues/1294 func (suite *TendermintTestSuite) TestVerifyNextSeqRecv() { var ( clientState *types.ClientState diff --git a/testing/chain.go b/testing/chain.go index 9e248312bc8..769acd8f7d1 100644 --- a/testing/chain.go +++ b/testing/chain.go @@ -200,11 +200,18 @@ func (chain *TestChain) QueryProof(key []byte) ([]byte, clienttypes.Height) { return chain.QueryProofAtHeight(key, chain.App.LastBlockHeight()) } -// QueryProof performs an abci query with the given key and returns the proto encoded merkle proof -// for the query and the height at which the proof will succeed on a tendermint verifier. +// QueryProofAtHeight performs an abci query with the given key and returns the proto encoded merkle proof +// for the query and the height at which the proof will succeed on a tendermint verifier. Only the IBC +// store is supported func (chain *TestChain) QueryProofAtHeight(key []byte, height int64) ([]byte, clienttypes.Height) { + return chain.QueryProofForStore(host.StoreKey, key, height) +} + +// QueryProofForStore performs an abci query with the given key and returns the proto encoded merkle proof +// for the query and the height at which the proof will succeed on a tendermint verifier. +func (chain *TestChain) QueryProofForStore(storeKey string, key []byte, height int64) ([]byte, clienttypes.Height) { res := chain.App.Query(abci.RequestQuery{ - Path: fmt.Sprintf("store/%s/key", host.StoreKey), + Path: fmt.Sprintf("store/%s/key", storeKey), Height: height - 1, Data: key, Prove: true,