Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add unreceived acks rpc #7562

Merged
merged 2 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/core/04-channel/v2/client/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func GetQueryCmd() *cobra.Command {
getCmdQueryPacketCommitments(),
getCmdQueryPacketAcknowledgement(),
getCmdQueryPacketReceipt(),
getCmdQueryUnreceivedAcks(),
)

return queryCmd
Expand Down
53 changes: 53 additions & 0 deletions modules/core/04-channel/v2/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import (
"github.com/cosmos/ibc-go/v9/modules/core/exported"
)

const (
flagSequences = "sequences"
)

// getCmdQueryChannel defines the command to query the channel information (creator and channel) for the given channel ID.
func getCmdQueryChannel() *cobra.Command {
cmd := &cobra.Command{
Expand Down Expand Up @@ -282,3 +286,52 @@ func getCmdQueryPacketReceipt() *cobra.Command {

return cmd
}

// getCmdQueryUnreceivedAcks defines the command to query all the unreceived acks on the original sending chain
func getCmdQueryUnreceivedAcks() *cobra.Command {
cmd := &cobra.Command{
Use: "unreceived-acks [channel-id]",
Short: "Query all the unreceived acks associated with a channel",
Long: `Given a list of acknowledgement sequences from counterparty, determine if an ack on the counterparty chain has been received on the executing chain.

The return value represents:
- Unreceived packet acknowledgement: packet commitment exists on original sending (executing) chain and ack exists on receiving chain.
`,
Example: fmt.Sprintf("%s query %s %s unreceived-acks [channel-id] --sequences=1,2,3", version.AppName, exported.ModuleName, types.SubModuleName),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)

seqSlice, err := cmd.Flags().GetInt64Slice(flagSequences)
if err != nil {
return err
}

seqs := make([]uint64, len(seqSlice))
for i := range seqSlice {
seqs[i] = uint64(seqSlice[i])
}

req := &types.QueryUnreceivedAcksRequest{
ChannelId: args[0],
PacketAckSequences: seqs,
}

res, err := queryClient.UnreceivedAcks(cmd.Context(), req)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

cmd.Flags().Int64Slice(flagSequences, []int64{}, "comma separated list of packet sequence numbers")
flags.AddQueryFlagsToCmd(cmd)

return cmd
}
52 changes: 52 additions & 0 deletions modules/core/04-channel/v2/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,55 @@ func (q *queryServer) PacketReceipt(ctx context.Context, req *types.QueryPacketR

return types.NewQueryPacketReceiptResponse(hasReceipt, nil, clienttypes.GetSelfHeight(ctx)), nil
}

// UnreceivedAcks implements the Query/UnreceivedAcks gRPC method. Given
// a list of counterparty packet acknowledgements, the querier checks if the packet
// has already been received by checking if the packet commitment still exists on this
// chain (original sender) for the packet sequence.
// All acknowledgmeents that haven't been received yet are returned in the response.
// Usage: To use this method correctly, first query all packet acknowledgements on
// the original receiving chain (ie the chain that wrote the acks) using the Query/PacketAcknowledgements gRPC method.
// Then input the returned sequences into the QueryUnreceivedAcksRequest
// and send the request to this Query/UnreceivedAcks on the **original sending**
// chain. This gRPC method will then return the list of packet sequences whose
// acknowledgements are already written on the receiving chain but haven't yet
// been received back to the sending chain.
//
// NOTE: The querier makes the assumption that the provided list of packet
// acknowledgements is correct and will not function properly if the list
// is not up to date. Ideally the query height should equal the latest height
// on the counterparty's client which represents this chain.
func (q *queryServer) UnreceivedAcks(ctx context.Context, req *types.QueryUnreceivedAcksRequest) (*types.QueryUnreceivedAcksResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}

if err := host.ChannelIdentifierValidator(req.ChannelId); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

if !q.HasChannel(ctx, req.ChannelId) {
return nil, status.Error(codes.NotFound, errorsmod.Wrap(types.ErrChannelNotFound, req.ChannelId).Error())
}

var unreceivedSequences []uint64

for _, seq := range req.PacketAckSequences {
if seq == 0 {
return nil, status.Error(codes.InvalidArgument, "packet sequence cannot be 0")
}

// if packet commitment still exists on the original sending chain, then packet ack has not been received
// since processing the ack will delete the packet commitment
if commitment := q.GetPacketCommitment(ctx, req.ChannelId, seq); len(commitment) != 0 {
unreceivedSequences = append(unreceivedSequences, seq)
}

}

selfHeight := clienttypes.GetSelfHeight(ctx)
return &types.QueryUnreceivedAcksResponse{
Sequences: unreceivedSequences,
Height: selfHeight,
}, nil
}
123 changes: 123 additions & 0 deletions modules/core/04-channel/v2/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,3 +586,126 @@ func (suite *KeeperTestSuite) TestQueryNextSequenceSend() {
})
}
}

func (suite *KeeperTestSuite) TestQueryUnreceivedAcks() {
var (
path *ibctesting.Path
req *types.QueryUnreceivedAcksRequest
expSeq = []uint64{}
)

testCases := []struct {
msg string
malleate func()
expError error
}{
{
"success",
func() {
expSeq = []uint64(nil)
req = &types.QueryUnreceivedAcksRequest{
ChannelId: path.EndpointA.ChannelID,
PacketAckSequences: []uint64{1},
}
},
nil,
},
{
"success: single unreceived packet ack",
func() {
suite.chainA.App.GetIBCKeeper().ChannelKeeperV2.SetPacketCommitment(suite.chainA.GetContext(), path.EndpointA.ChannelID, 1, []byte("commitment"))

expSeq = []uint64{1}
req = &types.QueryUnreceivedAcksRequest{
ChannelId: path.EndpointA.ChannelID,
PacketAckSequences: []uint64{1},
}
},
nil,
},
{
"success: multiple unreceived packet acknowledgements",
func() {
expSeq = []uint64{} // reset
packetAcks := []uint64{}

// set packet commitment for every other sequence
for seq := uint64(1); seq < 10; seq++ {
packetAcks = append(packetAcks, seq)

if seq%2 == 0 {
suite.chainA.App.GetIBCKeeper().ChannelKeeperV2.SetPacketCommitment(suite.chainA.GetContext(), path.EndpointA.ChannelID, seq, []byte("commitement"))
expSeq = append(expSeq, seq)
}
}

req = &types.QueryUnreceivedAcksRequest{
ChannelId: path.EndpointA.ChannelID,
PacketAckSequences: packetAcks,
}
},
nil,
},
{
"empty request",
func() {
req = nil
},
status.Error(codes.InvalidArgument, "empty request"),
},
{
"invalid channel ID",
func() {
req = &types.QueryUnreceivedAcksRequest{
ChannelId: "",
}
},
status.Error(codes.InvalidArgument, "identifier cannot be blank: invalid identifier"),
},
{
"channel not found",
func() {
req = &types.QueryUnreceivedAcksRequest{
ChannelId: "test-channel-id",
}
},
status.Error(codes.NotFound, fmt.Sprintf("%s: channel not found", "test-channel-id")),
},
{
"invalid seq",
func() {
req = &types.QueryUnreceivedAcksRequest{
ChannelId: path.EndpointA.ChannelID,
PacketAckSequences: []uint64{0},
}
},
status.Error(codes.InvalidArgument, "packet sequence cannot be 0"),
},
}

for _, tc := range testCases {
tc := tc

suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
path = ibctesting.NewPath(suite.chainA, suite.chainB)
path.SetupV2()

tc.malleate()
ctx := suite.chainA.GetContext()

queryServer := keeper.NewQueryServer(suite.chainA.App.GetIBCKeeper().ChannelKeeperV2)
res, err := queryServer.UnreceivedAcks(ctx, req)

expPass := tc.expError == nil
if expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
suite.Require().Equal(expSeq, res.Sequences)
} else {
suite.Require().ErrorIs(err, tc.expError)
suite.Require().Nil(res)
}
})
}
}
Loading
Loading