diff --git a/modules/core/02-client/keeper/grpc_query.go b/modules/core/02-client/keeper/grpc_query.go index 12daed284c7..de776ee4b66 100644 --- a/modules/core/02-client/keeper/grpc_query.go +++ b/modules/core/02-client/keeper/grpc_query.go @@ -328,3 +328,64 @@ func (k Keeper) UpgradedConsensusState(c context.Context, req *types.QueryUpgrad UpgradedConsensusState: protoAny, }, nil } +<<<<<<< HEAD +======= + +// VerifyMembership implements the Query/VerifyMembership gRPC method +// NOTE: Any state changes made within this handler are discarded by leveraging a cached context. Gas is consumed for underlying state access. +// This gRPC method is intended to be used within the context of the state machine and delegates to light clients to verify proofs. +func (k Keeper) VerifyMembership(c context.Context, req *types.QueryVerifyMembershipRequest) (*types.QueryVerifyMembershipResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := host.ClientIdentifierValidator(req.ClientId); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + if len(req.Proof) == 0 { + return nil, status.Error(codes.InvalidArgument, "empty proof") + } + + if req.ProofHeight.IsZero() { + return nil, status.Error(codes.InvalidArgument, "proof height must be non-zero") + } + + if req.MerklePath.Empty() { + return nil, status.Error(codes.InvalidArgument, "empty merkle path") + } + + if len(req.Value) == 0 { + return nil, status.Error(codes.InvalidArgument, "empty value") + } + + ctx := sdk.UnwrapSDKContext(c) + // cache the context to ensure clientState.VerifyMembership does not change state + cachedCtx, _ := ctx.CacheContext() + + // make sure we charge the higher level context even on panic + defer func() { + ctx.GasMeter().ConsumeGas(cachedCtx.GasMeter().GasConsumed(), "verify membership query") + }() + + clientState, found := k.GetClientState(cachedCtx, req.ClientId) + if !found { + return nil, status.Error(codes.NotFound, errorsmod.Wrap(types.ErrClientNotFound, req.ClientId).Error()) + } + + if clientStatus := k.GetClientStatus(ctx, clientState, req.ClientId); clientStatus != exported.Active { + return nil, status.Error(codes.FailedPrecondition, errorsmod.Wrapf(types.ErrClientNotActive, "cannot verify membership using client (%s) with status %s", req.ClientId, clientStatus).Error()) + } + + if err := clientState.VerifyMembership(cachedCtx, k.ClientStore(cachedCtx, req.ClientId), k.cdc, req.ProofHeight, req.TimeDelay, req.BlockDelay, req.Proof, req.MerklePath, req.Value); err != nil { + k.Logger(ctx).Debug("proof verification failed", "key", req.MerklePath, "error", err) + return &types.QueryVerifyMembershipResponse{ + Success: false, + }, nil + } + + return &types.QueryVerifyMembershipResponse{ + Success: true, + }, nil +} +>>>>>>> 94a4597c (chore: add client status check to verify membership rpc (#5870)) diff --git a/modules/core/02-client/keeper/grpc_query_test.go b/modules/core/02-client/keeper/grpc_query_test.go index 231d000a257..0139a2248bb 100644 --- a/modules/core/02-client/keeper/grpc_query_test.go +++ b/modules/core/02-client/keeper/grpc_query_test.go @@ -659,3 +659,162 @@ func (suite *KeeperTestSuite) TestQueryClientParams() { res, _ := suite.chainA.QueryServer.ClientParams(ctx, &types.QueryClientParamsRequest{}) suite.Require().Equal(&expParams, res.Params) } +<<<<<<< HEAD +======= + +func (suite *KeeperTestSuite) TestQueryVerifyMembershipProof() { + var ( + path *ibctesting.Path + req *types.QueryVerifyMembershipRequest + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success", + func() { + channel := path.EndpointB.GetChannel() + bz, err := suite.chainB.Codec.Marshal(&channel) + suite.Require().NoError(err) + + channelProof, proofHeight := path.EndpointB.QueryProof(host.ChannelKey(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)) + + merklePath := commitmenttypes.NewMerklePath(host.ChannelPath(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)) + merklePath, err = commitmenttypes.ApplyPrefix(suite.chainB.GetPrefix(), merklePath) + suite.Require().NoError(err) + + req = &types.QueryVerifyMembershipRequest{ + ClientId: path.EndpointA.ClientID, + Proof: channelProof, + ProofHeight: proofHeight, + MerklePath: merklePath, + Value: bz, + } + }, + nil, + }, + { + "req is nil", + func() { + req = nil + }, + errors.New("empty request"), + }, + { + "invalid client ID", + func() { + req = &types.QueryVerifyMembershipRequest{ + ClientId: "//invalid_id", + } + }, + host.ErrInvalidID, + }, + { + "empty proof", + func() { + req = &types.QueryVerifyMembershipRequest{ + ClientId: ibctesting.FirstClientID, + Proof: []byte{}, + } + }, + errors.New("empty proof"), + }, + { + "invalid proof height", + func() { + req = &types.QueryVerifyMembershipRequest{ + ClientId: ibctesting.FirstClientID, + Proof: []byte{0x01}, + ProofHeight: types.ZeroHeight(), + } + }, + errors.New("proof height must be non-zero"), + }, + { + "empty merkle path", + func() { + req = &types.QueryVerifyMembershipRequest{ + ClientId: ibctesting.FirstClientID, + Proof: []byte{0x01}, + ProofHeight: types.NewHeight(1, 100), + } + }, + errors.New("empty merkle path"), + }, + { + "empty value", + func() { + req = &types.QueryVerifyMembershipRequest{ + ClientId: ibctesting.FirstClientID, + Proof: []byte{0x01}, + ProofHeight: types.NewHeight(1, 100), + MerklePath: commitmenttypes.NewMerklePath("/ibc", host.ChannelPath(mock.PortID, ibctesting.FirstChannelID)), + } + }, + errors.New("empty value"), + }, + { + "client not found", + func() { + req = &types.QueryVerifyMembershipRequest{ + ClientId: types.FormatClientIdentifier(exported.Tendermint, 100), // use a sequence which hasn't been created yet + Proof: []byte{0x01}, + ProofHeight: types.NewHeight(1, 100), + MerklePath: commitmenttypes.NewMerklePath("/ibc", host.ChannelPath(mock.PortID, ibctesting.FirstChannelID)), + Value: []byte{0x01}, + } + }, + types.ErrClientNotFound, + }, + { + "client not active", + func() { + params := types.NewParams("") // disable all clients + suite.chainA.GetSimApp().GetIBCKeeper().ClientKeeper.SetParams(suite.chainA.GetContext(), params) + + req = &types.QueryVerifyMembershipRequest{ + ClientId: path.EndpointA.ClientID, + Proof: []byte{0x01}, + ProofHeight: types.NewHeight(1, 100), + MerklePath: commitmenttypes.NewMerklePath("/ibc", host.ChannelPath(mock.PortID, ibctesting.FirstChannelID)), + Value: []byte{0x01}, + } + }, + types.ErrClientNotActive, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupTest() // reset + + path = ibctesting.NewPath(suite.chainA, suite.chainB) + path.Setup() + + tc.malleate() + + ctx := suite.chainA.GetContext() + initialGas := ctx.GasMeter().GasConsumed() + res, err := suite.chainA.QueryServer.VerifyMembership(ctx, req) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + suite.Require().True(res.Success, "failed to verify membership proof") + + gasConsumed := ctx.GasMeter().GasConsumed() + suite.Require().Greater(gasConsumed, initialGas, "gas consumed should be greater than initial gas") + } else { + suite.Require().ErrorContains(err, tc.expError.Error()) + + gasConsumed := ctx.GasMeter().GasConsumed() + suite.Require().GreaterOrEqual(gasConsumed, initialGas, "gas consumed should be greater than or equal to initial gas") + } + }) + } +} +>>>>>>> 94a4597c (chore: add client status check to verify membership rpc (#5870))