# Create a custom IBC middleware
IBC middleware will wrap over an underlying IBC application (a base application or downstream middleware) and sits between core IBC and the base application.
The interfaces a middleware must implement are found here (opens new window).
An IBCMiddleware
struct implementing the Middleware
interface, can be defined with its constructor as follows:
# Implement IBCModule
interface
IBCMiddleware
is a struct that implements the ICS-26 IBCModule
interface (porttypes.IBCModule
) (opens new window). It is recommended to separate these callbacks into a separate file ibc_middleware.go
.
Note how this is analogous to implementing the same interfaces for IBC applications that act as base applications.
As will be mentioned in the integration section, this struct should be different than the struct that implements AppModule
in case the middleware maintains its own internal state and processes separate SDK messages.
The middleware must have access to the underlying application, and be called before it during all ICS-26 callbacks. It may execute custom logic during these callbacks, and then call the underlying application's callback.
Middleware may choose not to call the underlying application's callback at all. Though these should generally be limited to error cases.
The IBCModule
interface consists of the channel handshake callbacks and packet callbacks. Most of the custom logic will be performed in the packet callbacks, in the case of the channel handshake callbacks, introducing the middleware requires consideration to the version negotiation and passing of capabilities.
# Channel handshake callbacks
# Version negotiation
In the case where the IBC middleware expects to speak to a compatible IBC middleware on the counterparty chain, they must use the channel handshake to negotiate the middleware version without interfering in the version negotiation of the underlying application.
Middleware accomplishes this by formatting the version in a JSON-encoded string containing the middleware version and the application version. The application version may as well be a JSON-encoded string, possibly including further middleware and app versions, if the application stack consists of multiple milddlewares wrapping a base application. The format of the version is specified in ICS-30 as the following:
The <middleware_version_key>
key in the JSON struct should be replaced by the actual name of the key for the corresponding middleware (e.g. fee_version
).
During the handshake callbacks, the middleware can unmarshal the version string and retrieve the middleware and application versions. It can do its negotiation logic on <middleware_version_value>
, and pass the <application_version_value>
to the underlying application.
NOTE: Middleware that does not need to negotiate with a counterparty middleware on the remote stack will not implement the version unmarshalling and negotiation, and will simply perform its own custom logic on the callbacks without relying on the counterparty behaving similarly.
# OnChanOpenInit
See here (opens new window) an example implementation of this callback for the ICS-29 Fee Middleware module.
# OnChanOpenTry
See here (opens new window) an example implementation of this callback for the ICS-29 Fee Middleware module.
# OnChanOpenAck
See here (opens new window)) an example implementation of this callback for the ICS-29 Fee Middleware module.
# OnChanOpenConfirm
See here (opens new window) an example implementation of this callback for the ICS-29 Fee Middleware module.
# OnChanCloseInit
See here (opens new window) an example implementation of this callback for the ICS-29 Fee Middleware module.
# OnChanCloseConfirm
See here (opens new window) an example implementation of this callback for the ICS-29 Fee Middleware module.
# Capabilities
The middleware should simply pass the capability in the callback arguments along to the underlying application so that it may be claimed by the base application. The base application will then pass the capability up the stack in order to authenticate an outgoing packet/acknowledgement, which you can check in the ICS4Wrapper
section.
In the case where the middleware wishes to send a packet or acknowledgment without the involvement of the underlying application, it should be given access to the same scopedKeeper
as the base application so that it can retrieve the capabilities by itself.
# Packet callbacks
The packet callbacks just like the handshake callbacks wrap the application's packet callbacks. The packet callbacks are where the middleware performs most of its custom logic. The middleware may read the packet flow data and perform some additional packet handling, or it may modify the incoming data before it reaches the underlying application. This enables a wide degree of usecases, as a simple base application like token-transfer can be transformed for a variety of usecases by combining it with custom middleware.
# OnRecvPacket
See here (opens new window) an example implementation of this callback for the ICS-29 Fee Middleware module.
# OnAcknowledgementPacket
See here (opens new window) an example implementation of this callback for the ICS-29 Fee Middleware module.
# OnTimeoutPacket
See here (opens new window) an example implementation of this callback for the ICS-29 Fee Middleware module.
# ICS-04 wrappers
Middleware must also wrap ICS-04 so that any communication from the application to the channelKeeper
goes through the middleware first. Similar to the packet callbacks, the middleware may modify outgoing acknowledgements and packets in any way it wishes.
To ensure optimal generalisability, the ICS4Wrapper
abstraction serves to abstract away whether a middleware is the topmost middleware (and thus directly caling into the ICS-04 channelKeeper
) or itself being wrapped by another middleware.
Remember that middleware can be stateful or stateless. When defining the stateful middleware's keeper, the ics4Wrapper
field is included. Then the appropriate keeper can be passed when instantiating the middleware's keeper in app.go
For stateless middleware, the ics4Wrapper
can be passed on directly without having to instantiate a keeper struct for the middleware.
The interface (opens new window) looks as follows:
⚠️ In the following paragraphs, the methods are presented in pseudo code which has been kept general, not stating whether the middleware is stateful or stateless. Remember that when the middleware is stateful, ics4Wrapper
can be accessed through the keeper.
Check out the references provided for an actual implementation to clarify, where the ics4Wrapper
methods in ibc_middleware.go
simply call the equivalent keeper methods where the actual logic resides.
# SendPacket
See here (opens new window) an example implementation of this function for the ICS-29 Fee Middleware module.
# WriteAcknowledgement
See here (opens new window) an example implementation of this function for the ICS-29 Fee Middleware module.
# GetAppVersion
See here (opens new window) an example implementation of this function for the ICS-29 Fee Middleware module.