Skip to content

Commit

Permalink
feat: add rpc VerifyMembershipProof - querier approach for conditio…
Browse files Browse the repository at this point in the history
…nal clients (#5821)

* feat: adding protobuf msgs and rpc for VerifyMembershipProof

* feat: adding VerifyMembershipProof query implementation and wiring

* chore(08-wasm): add VerifyMembershipProof to stargate query acceptlist

* test: adding failure case unit tests for VerifyMembershipProof query

* fix: correct protodoc

* chore: proto-swagger-gen

* chore: protodocs

* test: adding additional test cases

* test: assert gas consumed in tests

* chore: rename rpc to VerifyMembership and update tests

* chore: update service definition URL in 08-wasm stargate accepted queries

* test: adding verify membership test to 08-wasm querier

* Update proto/ibc/core/client/v1/query.proto

Co-authored-by: Carlos Rodriguez <carlos@interchain.io>

* chore: review items - log error at debug, pass cachedCtx and adjust tests for discarded state checks

* chore: add doc comment to querier test, address nit to move defaultAcceptList

* chore: regen protos and swagger doc

* nit: update comment in querier

* imp: add more info to godoc for VerifyMembership rpc

---------

Co-authored-by: Carlos Rodriguez <carlos@interchain.io>
Co-authored-by: Cian Hatton <cian@interchain.io>
(cherry picked from commit ed9bf74)

# Conflicts:
#	docs/client/swagger-ui/swagger.yaml
#	modules/light-clients/08-wasm/types/querier.go
#	modules/light-clients/08-wasm/types/querier_test.go
  • Loading branch information
damiannolan authored and mergify[bot] committed Apr 8, 2024
1 parent 157e2a3 commit 397bcbb
Show file tree
Hide file tree
Showing 9 changed files with 22,936 additions and 67 deletions.
21,393 changes: 21,393 additions & 0 deletions docs/client/swagger-ui/swagger.yaml

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions modules/core/02-client/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,57 @@ func (k Keeper) UpgradedConsensusState(c context.Context, req *types.QueryUpgrad
UpgradedConsensusState: protoAny,
}, nil
}

// 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 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
}
144 changes: 144 additions & 0 deletions modules/core/02-client/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package keeper_test

import (
"errors"
"fmt"

codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/types/query"

"github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types"
host "github.com/cosmos/ibc-go/v8/modules/core/24-host"
"github.com/cosmos/ibc-go/v8/modules/core/exported"
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
ibctesting "github.com/cosmos/ibc-go/v8/testing"
"github.com/cosmos/ibc-go/v8/testing/mock"
)

func (suite *KeeperTestSuite) TestQueryClientState() {
Expand Down Expand Up @@ -658,3 +662,143 @@ func (suite *KeeperTestSuite) TestQueryClientParams() {
res, _ := suite.chainA.QueryServer.ClientParams(ctx, &types.QueryClientParamsRequest{})
suite.Require().Equal(&expParams, res.Params)
}

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,
},
}

for _, tc := range testCases {
tc := tc
suite.Run(tc.name, func() {
suite.SetupTest() // reset

path = ibctesting.NewPath(suite.chainA, suite.chainB)
path.Setup()

Check failure on line 781 in modules/core/02-client/keeper/grpc_query_test.go

View workflow job for this annotation

GitHub Actions / lint

path.Setup undefined (type *ibctesting.Path has no field or method Setup) (typecheck)

Check failure on line 781 in modules/core/02-client/keeper/grpc_query_test.go

View workflow job for this annotation

GitHub Actions / lint

path.Setup undefined (type *ibctesting.Path has no field or method Setup) (typecheck)

Check failure on line 781 in modules/core/02-client/keeper/grpc_query_test.go

View workflow job for this annotation

GitHub Actions / tests (01)

path.Setup undefined (type *ibctesting.Path has no field or method 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")
}
})
}
}
Loading

0 comments on commit 397bcbb

Please sign in to comment.