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

docs: update middleware documentation #1639

Merged
merged 12 commits into from
Jul 14, 2022
169 changes: 120 additions & 49 deletions docs/ibc/middleware/develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
order: 1
-->

# IBC Middleware
# IBC middleware
crodriguezvega marked this conversation as resolved.
Show resolved Hide resolved

Learn how to write your own custom middleware to wrap an IBC application, and understand how to hook different middleware to IBC base applications to form different IBC application stacks {synopsis}.

Expand All @@ -12,7 +12,7 @@ IBC applications are designed to be self-contained modules that implement their

Middleware allows developers to define the extensions as separate modules that can wrap over the base application. This middleware can thus perform its own custom logic, and pass data into the application so that it may run its logic without being aware of the middleware's existence. This allows both the application and the middleware to implement its own isolated logic while still being able to run as part of a single packet flow.

## Pre-requisite Readings
## Pre-requisite readings

- [IBC Overview](../overview.md) {prereq}
- [IBC Integration](../integration.md) {prereq}
Expand All @@ -28,9 +28,9 @@ Middleware allows developers to define the extensions as separate modules that c

`Application Stack (or stack)`: A stack is the complete set of application logic (middleware(s) + base application) that gets connected to core IBC. A stack may be just a base application, or it may be a series of middlewares that nest a base application.

## Create a custom IBC Middleware
## Create a custom IBC middleware

IBC Middleware will wrap over an underlying IBC application and sits between core IBC and the application. It has complete control in modifying any message coming from IBC to the application, and any message coming from the application to core IBC. Thus, middleware must be completely trusted by chain developers who wish to integrate them, however this gives them complete flexibility in modifying the application(s) they wrap.
IBC middleware will wrap over an underlying IBC application and sits between core IBC and the application. It has complete control in modifying any message coming from IBC to the application, and any message coming from the application to core IBC. Thus, middleware must be completely trusted by chain developers who wish to integrate them, however this gives them complete flexibility in modifying the application(s) they wrap.

#### Interfaces

Expand All @@ -48,22 +48,28 @@ type Middleware interface {
// which will call the next middleware until it reaches the core IBC handler.
type ICS4Wrapper interface {
SendPacket(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet exported.Packet) error
WriteAcknowledgement(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet exported.Packet, ack []byte) error
GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool)
WriteAcknowledgement(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet exported.Packet, ack exported.Acknowledgement) error
GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool)
}
```

### Implement `IBCModule` interface and callbacks

The IBCModule is struct that implements the ICS26Interface (`porttypes.IBCModule`). It is recommended to separate these callbacks into a separate file `ibc_module.go`. As will be mentioned in the [integration doc](./integration.md), 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 `IBCModule` is a struct that implements the [ICS-26 interface (`porttypes.IBCModule`)](https://github.com/cosmos/ibc-go/blob/main/modules/core/05-port/types/module.go#L11-L106). It is recommended to separate these callbacks into a separate file `ibc_module.go`. As will be mentioned in the [integration section](./integration.md), 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 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.

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.
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 the following format: `{mw-version}:{app-version}`.
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:

During the handshake callbacks, the middleware can split the version into: `mw-version`, `app-version`. It can do its negotiation logic on `mw-version`, and pass the `app-version` to the underlying application.
```json
{"<middleware_version_key>":"<middleware_version_value>","app_version":"<application_version_value>"}
```

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.

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.

Expand All @@ -80,20 +86,48 @@ func (im IBCModule) OnChanOpenInit(ctx sdk.Context,
channelCap *capabilitytypes.Capability,
counterparty channeltypes.Counterparty,
version string,
) error {
// core/04-channel/types contains a helper function to split middleware and underlying app version
middlewareVersion, appVersion = channeltypes.SplitChannelVersion(version)
) (string, error) {
// try to unmarshal JSON-encoded version string and pass
// the app-specific version to app callback.
// otherwise, pass version directly to app callback.
metadata, err := Unmarshal(version)
if err != nil {
// Since it is valid for fee version to not be specified, the above middleware version may be for a middleware
// pass the entire version string onto the underlying
// application.
return im.app.OnChanOpenInit(
ctx,
order,
connectionHops,
portID,
channelID,
channelCap,
counterparty,
version,
)
}

doCustomLogic()
im.app.OnChanOpenInit(

// if the version string is empty, OnChanOpenInit is expected to return
// a default version string representing the version(s) it supports
appVersion, err := im.app.OnChanOpenInit(
ctx,
order,
connectionHops,
portID,
channelID,
channelCap,
counterparty,
appVersion, // note we only pass app version here
metadata.AppVersion, // note we only pass app version here
)
if err != nil {
return "", err
}

version := constructVersion(metadata.MiddlewareVersion, appVersion)

return version, nil
}

func OnChanOpenTry(
Expand All @@ -108,10 +142,25 @@ func OnChanOpenTry(
) (string, error) {
doCustomLogic()

// core/04-channel/types contains a helper function to split middleware and underlying app version
cpMiddlewareVersion, cpAppVersion = channeltypes.SplitChannelVersion(counterpartyVersion)
// try to unmarshal JSON-encoded version string and pass
// the app-specific version to app callback.
// otherwise, pass version directly to app callback.
cpMetadata, err := Unmarshal(counterpartyVersion)
if err != nil {
return app.OnChanOpenTry(
ctx,
order,
connectionHops,
portID,
channelID,
channelCap,
counterparty,
counterpartyVersion,
)
}

// call the underlying applications OnChanOpenTry callback
// Call the underlying applications OnChanOpenTry callback.
// The try callback must select the final app-specific version string and return it.
appVersion, err := app.OnChanOpenTry(
ctx,
order,
Expand All @@ -120,33 +169,41 @@ func OnChanOpenTry(
channelID,
channelCap,
counterparty,
cpAppVersion, // note we only pass counterparty app version here
cpMetadata.AppVersion, // note we only pass counterparty app version here
)
if err != nil {
return err
return "", err
}

middlewareVersion := negotiateMiddlewareVersion(cpMiddlewareVersion)
// negotiate final middleware version
middlewareVersion := negotiateMiddlewareVersion(cpMetadata.MiddlewareVersion)
version := constructVersion(middlewareVersion, appVersion)

return version
return version, nil
}

func OnChanOpenAck(
ctx sdk.Context,
portID,
channelID string,
counterpartyChannelID string,
counterpartyVersion string,
) error {
// core/04-channel/types contains a helper function to split middleware and underlying app version
middlewareVersion, appVersion = channeltypes.SplitChannelVersion(version)
if !isCompatible(middlewareVersion) {
// try to unmarshal JSON-encoded version string and pass
// the app-specific version to app callback.
// otherwise, pass version directly to app callback.
cpMetadata, err = UnmarshalJSON(counterpartyVersion)
if err != nil {
return app.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion)
}

if !isCompatible(cpMetadata.MiddlewareVersion) {
return error
}
doCustomLogic()

// call the underlying applications OnChanOpenTry callback
app.OnChanOpenAck(ctx, portID, channelID, appVersion)
return app.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, cpMetadata.AppVersion)
}

func OnChanOpenConfirm(
Expand All @@ -156,93 +213,107 @@ func OnChanOpenConfirm(
) error {
doCustomLogic()

app.OnChanOpenConfirm(ctx, portID, channelID)
return app.OnChanOpenConfirm(ctx, portID, channelID)
}

OnChanCloseInit(
func OnChanCloseInit(
ctx sdk.Context,
portID,
channelID string,
) error {
doCustomLogic()

app.OnChanCloseInit(ctx, portID, channelID)
return app.OnChanCloseInit(ctx, portID, channelID)
}

OnChanCloseConfirm(
func OnChanCloseConfirm(
ctx sdk.Context,
portID,
channelID string,
) error {
doCustomLogic()

app.OnChanCloseConfirm(ctx, portID, channelID)
return app.OnChanCloseConfirm(ctx, portID, channelID)
}
```

NOTE: Middleware that does not need to negotiate with a counterparty middleware on the remote stack will not implement the version splitting and negotiation, and will simply perform its own custom logic on the callbacks without relying on the counterparty behaving similarly.
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.

### 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.

```go
OnRecvPacket(
func OnRecvPacket(
ctx sdk.Context,
packet channeltypes.Packet,
relayer sdk.AccAddress,
) ibcexported.Acknowledgement {
doCustomLogic(packet)

ack := app.OnRecvPacket(ctx, packet)
ack := app.OnRecvPacket(ctx, packet, relayer)

doCustomLogic(ack) // middleware may modify outgoing ack
return ack
}

OnAcknowledgementPacket(
func OnAcknowledgementPacket(
ctx sdk.Context,
packet channeltypes.Packet,
acknowledgement []byte,
) (*sdk.Result, error) {
relayer sdk.AccAddress,
) error {
doCustomLogic(packet, ack)

app.OnAcknowledgementPacket(ctx, packet, ack)
return app.OnAcknowledgementPacket(ctx, packet, ack, relayer)
}

OnTimeoutPacket(
func OnTimeoutPacket(
ctx sdk.Context,
packet channeltypes.Packet,
) (*sdk.Result, error) {
relayer sdk.AccAddress,
) error {
doCustomLogic(packet)

app.OnTimeoutPacket(ctx, packet)
return app.OnTimeoutPacket(ctx, packet, relayer)
}
```

### ICS-4 Wrappers
### ICS-4 wrappers

Middleware must also wrap ICS-4 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.
Middleware must also wrap ICS-4 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.

```go
// only called for async acks
func WriteAcknowledgement(
packet channeltypes.Packet,
acknowledgement []bytes) {
ctx sdk.Context,
chanCap *capabilitytypes.Capability,
packet exported.PacketI,
ack exported.Acknowledgement,
) {
// middleware may modify acknowledgement
ack_bytes = doCustomLogic(acknowledgement)
ack_bytes = doCustomLogic(ack)

return ics4Keeper.WriteAcknowledgement(packet, ack_bytes)
}

func SendPacket(appPacket channeltypes.Packet) {
func SendPacket(
ctx sdk.Context,
chanCap *capabilitytypes.Capability,
appPacket exported.PacketI,
) {
// middleware may modify packet
packet = doCustomLogic(app_packet)
packet = doCustomLogic(appPacket)

return ics4Keeper.SendPacket(packet)
return ics4Keeper.SendPacket(ctx, chanCap, packet)
}

// middleware must return the underlying application version
func GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) {
func GetAppVersion(
ctx sdk.Context,
portID,
channelID string,
) (string, bool) {
version, found := ics4Keeper.GetAppVersion(ctx, portID, channelID)
if !found {
return "", false
Expand Down
14 changes: 7 additions & 7 deletions docs/ibc/middleware/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
order: 2
-->

# Integrating IBC Middleware into a Chain
# Integrating IBC middleware into a chain

Learn how to integrate IBC middleware(s) with a base application to your chain. The following document only applies for Cosmos SDK chains.

Expand Down Expand Up @@ -46,18 +46,18 @@ scopedKeeperCustom2 := capabilityKeeper.NewScopedKeeper("custom2")
// initialize base IBC applications
// if you want to create two different stacks with the same base application,
// they must be given different scopedKeepers and assigned different ports.
transferIBCModule := transfer.NewIBCModule(transferKeeper, scopedKeeperTransfer)
customIBCModule1 := custom.NewIBCModule(customKeeper, scopedKeeperCustom1, "portCustom1")
customIBCModule2 := custom.NewIBCModule(customKeeper, scopedKeeperCustom2, "portCustom2")
transferIBCModule := transfer.NewIBCModule(transferKeeper)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am actually not sure about this part... Looking at the code, the NewIBCModule functions don't take a scoped keeper or port...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its fine to leave it out here. As far as I know its the transfer Keeper which takes the scoped keeper

customIBCModule1 := custom.NewIBCModule(customKeeper, "portCustom1")
customIBCModule2 := custom.NewIBCModule(customKeeper, "portCustom2")

// create IBC stacks by combining middleware with base application
// NOTE: since middleware2 is stateless it does not require a Keeper
// stack 1 contains mw1 -> mw3 -> transfer
stack1 := mw1.NewIBCModule(mw1Keeper, mw3.NewIBCModule(mw3Keeper, transferIBCModule))
stack1 := mw1.NewIBCMiddleware(mw3.NewIBCMiddleware(transferIBCModule, mw3Keeper), mw1Keeper)
// stack 2 contains mw3 -> mw2 -> custom1
stack2 := mw3.NewIBCModule(mw3Keeper, mw3.NewIBCModule(customIBCModule1))
stack2 := mw3.NewIBCMiddleware(mw2.NewIBCMiddleware(customIBCModule1), mw3Keeper)
// stack 3 contains mw2 -> mw1 -> custom2
stack3 := mw2.NewIBCModule(mw1.NewIBCModule(mw1Keeper, customIBCModule2))
stack3 := mw2.NewIBCMiddleware(mw1.NewIBCMiddleware(customIBCModule2, mw1Keeper))

// associate each stack with the moduleName provided by the underlying scopedKeeper
ibcRouter := porttypes.NewRouter()
Expand Down
2 changes: 1 addition & 1 deletion modules/apps/29-fee/ibc_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ func (im IBCMiddleware) OnRecvPacket(

ack := im.app.OnRecvPacket(ctx, packet, relayer)

// incase of async aknowledgement (ack == nil) store the relayer address for use later during async WriteAcknowledgement
// in case of async aknowledgement (ack == nil) store the relayer address for use later during async WriteAcknowledgement
if ack == nil {
im.keeper.SetRelayerAddressForAsyncAck(ctx, channeltypes.NewPacketId(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence()), relayer.String())
return nil
Expand Down