# Make a Module IBC-Enabled
In this section, you will build a conceptual Cosmos SDK blockchain with one module: first as a regular module, and second as an IBC module. This will introduce you to what makes a module IBC-enabled.
# Scaffold a leaderboard chain
By now you should be familiar with scaffolding a chain with Ignite CLI. If not, check out the Create Your Own Chain chapter.
To begin, scaffold a leaderboard
chain:
This creates a chain with x/leaderboard
as a regular SDK module.
Next, scaffold another chain (for example in another git branch), but this time add the --no-module
flag:
Now add the x/leaderboard
module as an IBC module with the --ibc
flag:
The output you see on the terminal when the module has finished scaffolding already gives a sense of what has to be implemented to create an IBC module:
The code in this section was scaffolded with Ignite CLI v0.22.1. This version includes ibc-go v3 as a dependency, which has reached past end-of-life (opens new window) and is no longer actively supported.
It is thus highly discouraged to deploy any code in production using ibc-go code scaffolded by Ignite CLI v0.22.1.
All actively supported versions of ibc-go (opens new window) have reached past v3, so there may be some differences compared to the code in this section. For documentation on the latest version of ibc-go, please refer to the ibc-go docs (opens new window).
For example, channel callbacks from v4 onwards now return a version string next to an error:
For a more detailed view, you can now compare both versions with a git diff
.
To make use of git diff
s to check the changes, be sure to commit between different (scaffolding) actions.
You can use git or GitHub to visualize the git diff
s or alternatively use diffy.org (opens new window).
# IBC application module requirements
What does Ignite CLI do behind the scenes when creating an IBC module for you? What do you need to implement if you want to upgrade a regular custom application module to an IBC-enabled module?
The required steps to implement can be found in the ibc-go docs (opens new window). There you will find:
To have your module interact over IBC you must:
- Implement the
IBCModule
interface:- Channel (opening) handshake callbacks
- Channel closing handshake callbacks
- Packet callbacks
- Bind to a port(s).
- Add keeper methods.
- Define your packet data and acknowledgement structs as well as how to encode/decode them.
- Add a route to the IBC router.
Now take a look at the git diff
and see if you can recognize the steps listed above.
# Implementing the IBCModule
interface
For a full explanation, visit the ibc-go docs (opens new window).
The Cosmos SDK expects all IBC modules to implement the IBCModule
interface (opens new window). This interface contains all of the callbacks IBC expects modules to implement. This includes callbacks related to:
- Channel handshake (
OnChanOpenInit
,OnChanOpenTry
,OnChanOpenAck
, andOnChanOpenConfirm
) - Channel closing (
OnChanCloseInit
andOnChanCloseConfirm
) - Packets (
OnRecvPacket
,OnAcknowledgementPacket
, andOnTimeoutPacket
).
Ignite CLI implements this in the file x/leaderboard/module_ibc.go
.
Additionally, in the module.go
file, the following line (and the corresponding import) will be added:
# Channel handshake version negotiation
Application modules are expected to verify the versioning used during the channel handshake procedure:
OnChanOpenInit
will verify that the relayer-chosen parameters are valid and perform any customINIT
logic.- It may return an error if the chosen parameters are invalid, in which case the handshake is aborted. If the provided version string is non-empty,
OnChanOpenInit
should return the version string if valid or an error if the provided version is invalid. - If the version string is empty,
OnChanOpenInit
is expected to return a default version string representing the version(s) it supports. If there is no default version string for the application, it should return an error if the provided version is an empty string.
- It may return an error if the chosen parameters are invalid, in which case the handshake is aborted. If the provided version string is non-empty,
OnChanOpenTry
will verify the relayer-chosen parameters along with the counterparty-chosen version string and perform customTRY
logic.- If the relayer-chosen parameters are invalid, the callback must return an error to abort the handshake. If the counterparty-chosen version is not compatible with this module's supported versions, the callback must return an error to abort the handshake.
- If the versions are compatible, the try callback must select the final version string and return it to core IBC.
OnChanOpenTry
may also perform custom initialization logic.
OnChanOpenAck
will error if the counterparty selected version string is invalid and abort the handshake. It may also perform customACK
logic.
Versions must be strings but can implement any versioning structure. Often a simple template is used that combines the name of the application and an iteration number, like leaderboard-1
for the leaderboard IBC module.
However, the version string can also include metadata to indicate attributes of the channel you are supporting, like applicable middleware and the underlying app version. An example of this is the version string for middleware, which is discussed in this IBC section.
# Packet callbacks
The general application packet flow was discussed in a previous section (opens new window). As a refresher, let's take a look at the diagram:
The packet callbacks in the packet flow can now be identified by investigating the IBCModule
interface.
# Sending packets
Modules do not send packets through callbacks, since modules initiate the action of sending packets to the IBC module, as opposed to other parts of the packet flow where messages sent to the IBC module must trigger execution on the port-bound module through the use of callbacks. Thus, to send a packet a module simply needs to call SendPacket
on the IBCChannelKeeper
.
In order to prevent modules from sending packets on channels they do not own, IBC expects modules to pass in the correct channel capability for the packet's source channel.
For advanced readers, more on capabilities can be found in the ibc-go docs (opens new window) or the ADR on the Dynamic Capability Store (opens new window).
# Receiving packets
To handle receiving packets, the module must implement the OnRecvPacket
callback. This gets invoked by the IBC module after the packet has been proved valid and correctly processed by the IBC keepers. Thus, the OnRecvPacket
callback only needs to worry about making the appropriate state changes given the packet data without worrying about whether the packet is valid or not.
Modules may return to the IBC handler an acknowledgement which implements the Acknowledgement
interface. The IBC handler will then commit this acknowledgement of the packet so that a relayer may relay the acknowledgement back to the sender module.
The state changes that occurred during this callback will only be written if:
- The acknowledgement was successful as indicated by the
Success()
function of the acknowledgement. - The acknowledgement returned is nil, indicating that an asynchronous process is occurring.
Applications that process asynchronous acknowledgements must handle reverting state changes when appropriate. Any state changes that occurred during the OnRecvPacket
callback will be written for asynchronous acknowledgements.
In x/leaderboard/module_ibc.go
scaffolded by Ignite CLI you find OnRecvPacket
:
The dispatch packet switch statement is added by Ignite CLI. As it is stated in the docs, strictly speaking, you only need to decode the packet data (which is discussed in an upcoming section) and return the acknowledgement after processing the packet. However, the structure provided by Ignite CLI is useful to get set up but can be changed according to the preference of the developer.
As a reminder, this is the Acknowledgement
interface:
# Acknowledging packets
The last step of the packet flow depends on whether you have a happy path, when the packet has been successfully relayed, or a timeout when something went wrong.
After a module writes an Acknowledgement
, a relayer can relay it back to the sender module. The sender module can then process the acknowledgement using the OnAcknowledgementPacket
callback. The contents of the Acknowledgement
are entirely up to the modules on the channel (just like the packet data); however, it may often contain information on whether the packet was successfully processed, along with some additional data that could be useful for remediation if the packet processing failed.
Since the modules are responsible for agreeing on an encoding/decoding standard for packet data and acknowledgements, IBC will pass in the acknowledgements as []byte
to this callback. The callback is responsible for decoding the acknowledgement and processing it.
In x/leaderboard/module_ibc.go
scaffolded by Ignite CLI you will find OnAcknowledgementPacket
:
Again, the structure to dispatch the packet with the switch statement as well as the switch statement for the ack (success case or error case) have been structured by Ignite CLI where the docs (opens new window) offer more freedom to the developer to implement decoding and processing of the ack.
The events that are being emitted are defined in x/leaderboard/types/events_ibc.go
.
# Timing out packets
If the timeout for a packet is reached before the packet is successfully received, or the counterparty channel end is closed before the packet is successfully received, then the receiving chain can no longer process it. Thus, the sending chain must process the timeout using OnTimeoutPacket
to handle this situation. Again the IBC module will verify that the timeout is indeed valid, so our module only needs to implement the state machine logic for what to do once a timeout is reached and the packet can no longer be received.
In x/leaderboard/module_ibc.go
scaffolded by Ignite CLI you will find OnTimeoutPacket
:
# Binding to a port
Every IBC module binds to a port, with a unique portID
which denotes the type of application.
The portID
does not refer to a certain numerical ID, like localhost:8080
with a portID
8080. Rather it refers to the application module to which the port binds. For IBC modules built with the Cosmos SDK it defaults to the module's name, and for CosmWasm contracts, it defaults to the contract address.
Currently, ports must be bound on app initialization. In order to bind modules to their respective ports on initialization, the following needs to be implemented:
Port ID in the
GenesisState
proto definition:Port ID as a key in the module store in
x/leaderboard/types/keys.go
:By default, the
portID
is indeed set to the module name, and the application version is set to<modulename>-n
withn
as an incrementing value.Port ID in the
x/leaderboard/types/genesis.go
:Binding of the IBC module to the port in
x/leaderboard/genesis.go
:Where:
The module binds to the desired port(s) and returns the capabilities.
# Keeper
Previous steps sometimes referenced keeper methods that deal with binding to and getting and setting a port, claiming and authenticating capabilities. These methods need to be added to the keeper.
For a full overview, check out the ibc-go docs (opens new window) and compare it with the x/leaderboard/keeper/keeper.go
file.
You will notice that Ignite CLI uses a custom cosmosibckeeper
package which you can find here (opens new window).
# Routing and app.go
When looking at app.go
you will see some minor additions, the most prominent of which is adding a route to the Leaderboard
module on the IBC Router
.
To summarize, this section has explored:
- How to build an SDK blockchain, as a regular module.
- How to build an SDK blockchain, as an IBC module.
- How application modules verify the versioning used during the channel handshake procedure.
- Packet callbacks, and specifically how sending, receiving, acknowledging, and timing out is handled in the packet flow.
- How to bind to a port, and the necessity of adding appropriate keeper methods to the keeper.