Filters

# Adding Packet and Acknowledgement Data

This section demonstrates how to define packets and acks (acknowledgements) for the leaderboard blockchain.

This blockchain will mostly be receiving packets from the checkers blockchain or other gaming chains, not sending them. This will be handled in the checkers blockchain extension section.

In this section, you will add an additional packet definition that will enable the Leaderboard chain to send a packet to connected game chains when a player has entered the top of the rankings. It is up to you if you want to include this in the Leaderboard chain you will build in the checkers extension exercise.

The documentation on how to define packets and acks in the Inter-Blockchain Communication Protocol (IBC) can be found in the ibc-go docs (opens new window).

# Scaffold a packet with Ignite CLI

You are now going to scaffold the IBC packet data with Ignite CLI, and compare it once more with git diff:

The packet is called ibcTopRank, which includes the fields playerId, rank, and score. Additionally, you send back the playerId of the player who entered the top of the rankings through the Acknowledgement.

The output on the terminal gives an overview of the changes made:

Copy modify proto/leaderboard/packet.proto modify proto/leaderboard/tx.proto modify x/leaderboard/client/cli/tx.go create x/leaderboard/client/cli/tx_ibc_top_rank.go modify x/leaderboard/handler.go create x/leaderboard/keeper/ibc_top_rank.go create x/leaderboard/keeper/msg_server_ibc_top_rank.go modify x/leaderboard/module_ibc.go modify x/leaderboard/types/codec.go modify x/leaderboard/types/events_ibc.go create x/leaderboard/types/messages_ibc_top_rank.go create x/leaderboard/types/messages_ibc_top_rank_test.go create x/leaderboard/types/packet_ibc_top_rank.go šŸŽ‰ Created a packet `ibcTopRank`.

In the next paragraphs, you will investigate each of the most important additions to the code.

# Proto definitions

The first additions are to the proto definitions in the packet.proto and tx.proto files:

Copy message LeaderboardPacketData { oneof packet { NoData noData = 1; // this line is used by starport scaffolding # ibc/packet/proto/field + IbcTopRankPacketData ibcTopRankPacket = 2; // this line is used by starport scaffolding # ibc/packet/proto/field/number } } separate leaderboard ... leaderboard packet.proto View source

One addition is IbcTopRankPacketData:

Copy // IbcTopRankPacketData defines a struct for the packet payload message IbcTopRankPacketData { string playerId = 1; uint64 rank = 2; uint64 score = 3; } separate leaderboard ... leaderboard packet.proto View source

The next addition is the ack:

Copy // IbcTopRankPacketAck defines a struct for the packet acknowledgement message IbcTopRankPacketAck { string playerId = 1; } separate leaderboard ... leaderboard packet.proto View source

And in tx.proto a Message service is added:

Copy // Msg defines the Msg service. service Msg { + rpc SendIbcTopRank(MsgSendIbcTopRank) returns (MsgSendIbcTopRankResponse); // this line is used by starport scaffolding # proto/tx/rpc } separate leaderboard ... leaderboard tx.proto View source

Where:

Copy message MsgSendIbcTopRank { string creator = 1; string port = 2; string channelID = 3; uint64 timeoutTimestamp = 4; string playerId = 5; uint64 rank = 6; uint64 score = 7; } message MsgSendIbcTopRankResponse { } separate leaderboard ... leaderboard tx.proto View source

The proto message MsgSendIbcTopRank includes the field timeoutTimestamp, which is added by Ignite CLI when scaffolding an IBC packet. This is an IBC channel parameter that is important in IBC and Ignite CLI abstracts this away, removing the need for the user to add this manually.

The proto definitions will be compiled into types/packet.pb.go and types/tx.pb.go.

# CLI commands

Ignite CLI also creates CLI commands to send packets and adds them to the client/cli/ folder.

Packets can be sent from the CLI with the following command:

Copy $ leaderboardd tx leaderboard send-ibc-top-rank \ [portID] [channelID] [playerId] [rank] [score]

# SendPacket and packet callback logic

When scaffolding an IBC module with Ignite CLI, you already saw the implementation of the IBCModule interface, including a bare-bones packet callbacks structure. Now that you have also scaffolded a packet (and ack), the callbacks have been added with logic to handle the receive, ack, and timeout scenarios.

Additionally, for the sending of a packet, a message server has been added that handles a SendPacket message, in this case MsgSendIbcTopRank.

IBC allows some freedom to the developers regarding how to implement the custom logic, decoding and encoding packets, and processing acks. The provided structure is but one example of how to tackle this. Therefore, it makes sense to focus on the general flow to handle user messages or IBC callbacks rather than the specific implementation by Ignite CLI.

# Sending packets

To handle a user submitting a message to send an IBC packet, a message server is added to the handler:

Copy func NewHandler(k keeper.Keeper) sdk.Handler { + msgServer := keeper.NewMsgServerImpl(k) return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { ctx = ctx.WithEventManager(sdk.NewEventManager()) switch msg := msg.(type) { + case *types.MsgSendIbcTopRank: + res, err := msgServer.SendIbcTopRank(sdk.WrapSDKContext(ctx), msg) + return sdk.WrapServiceResult(ctx, res, err) // this line is used by starport scaffolding # 1 default: errMsg := fmt.Sprintf("unrecognized %s message type: %T", types.ModuleName, msg) return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg) } } } separate leaderboard ... leaderboard handler.go View source

It calls the SendIbcTopRank method on the message server, defined as:

Copy func (k msgServer) SendIbcTopRank(goCtx context.Context, msg *types.MsgSendIbcTopRank) (*types.MsgSendIbcTopRankResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) // TODO: logic before transmitting the packet // Construct the packet var packet types.IbcTopRankPacketData packet.PlayerId = msg.PlayerId packet.Rank = msg.Rank packet.Score = msg.Score // Transmit the packet err := k.TransmitIbcTopRankPacket( ctx, packet, msg.Port, msg.ChannelID, clienttypes.ZeroHeight(), msg.TimeoutTimestamp, ) if err != nil { return nil, err } return &types.MsgSendIbcTopRankResponse{}, nil } separate leaderboard ... keeper msg_server_ibc_top_rank.go View source

This in turn calls the TransmitIbcTopRankPacket method on the module's keeper, defined in x/leaderboard/keeper/ibc_top_rank.go. This method gets all of the required metadata from core IBC before sending the packet using the ChannelKeeper's SendPacket function:

Copy func (k Keeper) TransmitIbcTopRankPacket( ctx sdk.Context, packetData types.IbcTopRankPacketData, sourcePort, sourceChannel string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, ) error { sourceChannelEnd, found := k.ChannelKeeper.GetChannel(ctx, sourcePort, sourceChannel) ... // error validation destinationPort := sourceChannelEnd.GetCounterparty().GetPortID() destinationChannel := sourceChannelEnd.GetCounterparty().GetChannelID() // get the next sequence sequence, found := k.ChannelKeeper.GetNextSequenceSend(ctx, sourcePort, sourceChannel) ... // error validation channelCap, ok := k.ScopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, sourceChannel)) ... // error validation packetBytes, err := packetData.GetBytes() ... // error validation packet := channeltypes.NewPacket( packetBytes, sequence, sourcePort, sourceChannel, destinationPort, destinationChannel, timeoutHeight, timeoutTimestamp, ) if err := k.ChannelKeeper.SendPacket(ctx, channelCap, packet); err != nil { return err } return nil } separate leaderboard ... keeper ibc_top_rank.go View source

When you want to add additional custom logic before transmitting the packet, you do this in the SendIbcTopRank method on the message server.

# Receiving packets

In a previous section you examined the OnRecvPacket callback in the x/leaderboard/module_ibc.go file. There, Ignite CLI had set up a structure to dispatch the packet depending on packet type through a switch statement. Now by adding the IbcTopRank packet, a case has been added:

Copy // @ switch packet := modulePacketData.Packet.(type) in OnRecvPacket case *types.LeaderboardPacketData_IbcTopRankPacket: packetAck, err := am.keeper.OnRecvIbcTopRankPacket(ctx, modulePacket, *packet.IbcTopRankPacket) if err != nil { ack = channeltypes.NewErrorAcknowledgement(err.Error()) } else { // Encode packet acknowledgement packetAckBytes, err := types.ModuleCdc.MarshalJSON(&packetAck) if err != nil { return channeltypes.NewErrorAcknowledgement(sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()).Error()) } ack = channeltypes.NewResultAcknowledgement(sdk.MustSortJSON(packetAckBytes)) } ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeIbcTopRankPacket, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute(types.AttributeKeyAckSuccess, fmt.Sprintf("%t", err != nil)), ), ) separate leaderboard ... leaderboard module_ibc.go View source

The first line of code in the case statement calls the application's OnRecvIbcTopRankPacket callback on the keeper to process the reception of the packet:

Copy // OnRecvIbcTopRankPacket processes packet reception func (k Keeper) OnRecvIbcTopRankPacket(ctx sdk.Context, packet channeltypes.Packet, data types.IbcTopRankPacketData) (packetAck types.IbcTopRankPacketAck, err error) { // validate packet data upon receiving if err := data.ValidateBasic(); err != nil { return packetAck, err } // TODO: packet reception logic return packetAck, nil } separate leaderboard ... keeper ibc_top_rank.go View source

Remember that the OnRecvPacket callback writes an acknowledgement as well (this course covers the synchronous write ack case).

# Acknowledging packets

Similarly to the OnRecvPacket case before, Ignite CLI has already prepared the structure of the OnAcknowledgementPacket with the switch statement. Again, scaffolding the packet adds a case to the switch:

Copy // @ switch packet := modulePacketData.Packet.(type) in OnAcknowledgementPacket case *types.LeaderboardPacketData_IbcTopRankPacket: err := am.keeper.OnAcknowledgementIbcTopRankPacket(ctx, modulePacket, *packet.IbcTopRankPacket, ack) if err != nil { return err } eventType = types.EventTypeIbcTopRankPacket

This calls into the newly created application keeper's ack packet callback:

Copy func (k Keeper) OnAcknowledgementIbcTopRankPacket(ctx sdk.Context, packet channeltypes.Packet, data types.IbcTopRankPacketData, ack channeltypes.Acknowledgement) error { switch dispatchedAck := ack.Response.(type) { case *channeltypes.Acknowledgement_Error: // TODO: failed acknowledgement logic _ = dispatchedAck.Error return nil case *channeltypes.Acknowledgement_Result: // Decode the packet acknowledgement var packetAck types.IbcTopRankPacketAck if err := types.ModuleCdc.UnmarshalJSON(dispatchedAck.Result, &packetAck); err != nil { // The counter-party module doesn't implement the correct acknowledgement format return errors.New("cannot unmarshal acknowledgement") } // TODO: successful acknowledgement logic return nil default: // The counter-party module doesn't implement the correct acknowledgement format return errors.New("invalid acknowledgement format") } } separate leaderboard ... keeper ibc_top_rank.go View source

This allows us to add custom application logic for both failed and successful acks.

# Timing out packets

Timing out the packets follows the same flow, adding a case to the switch statement in OnTimeoutPacket, calling into the keeper's timeout packet callback where the custom logic can be implemented. It is left to the reader to investigate this independently.

# Extra details

Next to the above, some additions have also been made to the types package. These include codec.go (opens new window), events_ibc.go (opens new window), and messages_ibc_top_rank.go (opens new window).

Again, the reader is invited to check these out independently.

Events in IBC are important because relayers process events to check if there are packets (or acknowledgements) to relay.

Ignite CLI has scaffolded some events in x/leaderboard/types/events_ibc.go for timeout and the ibcTopRank packet which you have defined:

Copy package types // IBC events const ( EventTypeTimeout = "timeout" EventTypeIbcTopRankPacket = "ibcTopRank_packet" // this line is used by starport scaffolding # ibc/packet/event AttributeKeyAckSuccess = "success" AttributeKeyAck = "acknowledgement" AttributeKeyAckError = "error" ) separate leaderboard ... types events_ibc.go View source

Here are found both the Event type and the attributes it contains.

These are not the only relevant events for IBC, though, the others can be found in the core IBC source code:

You can go back to the code examined so far to take note of the events emitted.

synopsis

To summarize, this section has explored:

  • Scaffolding a chain with an IBC-enabled module, with both chain and module called leaderboard.
  • How Ignite CLI made sure to implement the IBCModule interface, including channel handshake and packet callbacks.
  • How Ignite CLI has bound our IBC module to a port and added a route to the IBC router.
  • Scaffolding an IBC packet, IbcTopRankPacket.
  • How Ignite CLI defined the packet and ack data.
  • How Ignite CLI sets up the basic message handling and packet handling to send, receive, acknowledge, and timeout packets.

Even though the ability to send and receive packets is now enabled, no application logic to execute has yet been implemented. This is outside the scope of this section. The reader is invited to follow the checkers blockchain extension exercise.

When scaffolding a packet, Ignite CLI will ensure the chain can act both as the sender or receiver of a packet by default. This is a symmetrical setup which makes sense for some applications, like ICS20.

However, it is also possible to have an asymmetrical setup where one chain will always be the source or destination chain for a given packet, not both. In this case, the message server and packet callbacks can be updated to error when, for example, a chain receives a packet though it is supposed to exclusively be the destination chain. Interchain Accounts or ICS27 is an example of this asymmetrical situation, as is the checkers extension exercise.