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

Add extension point for ibc app version decoding #1100

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 1 addition & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ func NewWasmApp(

// Create fee enabled wasm ibc Stack
var wasmStack porttypes.IBCModule
wasmStack = wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper)
wasmStack = wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper, wasm.ICS29AppVersionDecoder(app.IBCFeeKeeper))
wasmStack = ibcfee.NewIBCMiddleware(wasmStack, app.IBCFeeKeeper)

// Create static IBC router, add app routes, then set and seal it
Expand Down
62 changes: 39 additions & 23 deletions x/wasm/ibc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ package wasm

import (
"math"
"strings"

wasmvmtypes "github.com/CosmWasm/wasmvm/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
ibcfees "github.com/cosmos/ibc-go/v4/modules/apps/29-fee/types"
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
porttypes "github.com/cosmos/ibc-go/v4/modules/core/05-port/types"
host "github.com/cosmos/ibc-go/v4/modules/core/24-host"
Expand All @@ -19,13 +17,21 @@ import (

var _ porttypes.IBCModule = IBCHandler{}

// IBCAppVersionDecoder abstract IBC app version decoder
type IBCAppVersionDecoder interface {
// Decode returns only the app version from the raw ibc version string.
// Implementations should strip down all middleware metadata
Decode(ctx sdk.Context, rawVersion, portID, channelID string) (string, error)
}

type IBCHandler struct {
keeper types.IBCContractKeeper
channelKeeper types.ChannelKeeper
keeper types.IBCContractKeeper
channelKeeper types.ChannelKeeper
versionDecoder IBCAppVersionDecoder
}

func NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper) IBCHandler {
return IBCHandler{keeper: k, channelKeeper: ck}
func NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper, d IBCAppVersionDecoder) IBCHandler {
return IBCHandler{keeper: k, channelKeeper: ck, versionDecoder: d}
}

// OnChanOpenInit implements the IBCModule interface
Expand Down Expand Up @@ -116,13 +122,13 @@ func (i IBCHandler) OnChanOpenTry(
return "", err
}
if version == "" {
version = counterpartyVersion
version = counterpartyVersion // as it was not rejected
}

// Module may have already claimed capability in OnChanOpenInit in the case of crossing hellos
// (ie chainA and chainB both call ChanOpenInit before one of them calls ChanOpenTry)
// If module can already authenticate the capability then module already owns it so we don't need to claim
// Otherwise, module does not have channel capability and we must claim it from IBC
// If module can already authenticate the capability then module already owns it, so we don't need to claim
// Otherwise, module does not have channel capability, and we must claim it from IBC
if !i.keeper.AuthenticateCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)) {
// Only claim channel capability passed back by IBC module if we do not already own it
if err := i.keeper.ClaimCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)); err != nil {
Expand Down Expand Up @@ -155,9 +161,14 @@ func (i IBCHandler) OnChanOpenAck(
// OnChanOpenAck entry point)
// https://github.com/cosmos/ibc-go/pull/647/files#diff-54b5be375a2333c56f2ae1b5b4dc13ac9c734561e30286505f39837ee75762c7R25
i.channelKeeper.SetChannel(ctx, portID, channelID, channelInfo)

appVersion, err := i.versionDecoder.Decode(ctx, channelInfo.Version, portID, channelID)
if err != nil {
return err
}
msg := wasmvmtypes.IBCChannelConnectMsg{
OpenAck: &wasmvmtypes.IBCOpenAck{
Channel: toWasmVMChannel(portID, channelID, channelInfo),
Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion),
CounterpartyVersion: counterpartyVersion,
},
}
Expand All @@ -174,9 +185,14 @@ func (i IBCHandler) OnChanOpenConfirm(ctx sdk.Context, portID, channelID string)
if !ok {
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
}

appVersion, err := i.versionDecoder.Decode(ctx, channelInfo.Version, portID, channelID)
if err != nil {
return err
}
msg := wasmvmtypes.IBCChannelConnectMsg{
OpenConfirm: &wasmvmtypes.IBCOpenConfirm{
Channel: toWasmVMChannel(portID, channelID, channelInfo),
Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion),
},
}
return i.keeper.OnConnectChannel(ctx, contractAddr, msg)
Expand All @@ -193,8 +209,12 @@ func (i IBCHandler) OnChanCloseInit(ctx sdk.Context, portID, channelID string) e
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
}

appVersion, err := i.versionDecoder.Decode(ctx, channelInfo.Version, portID, channelID)
if err != nil {
return err
}
msg := wasmvmtypes.IBCChannelCloseMsg{
CloseInit: &wasmvmtypes.IBCCloseInit{Channel: toWasmVMChannel(portID, channelID, channelInfo)},
CloseInit: &wasmvmtypes.IBCCloseInit{Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion)},
}
err = i.keeper.OnCloseChannel(ctx, contractAddr, msg)
if err != nil {
Expand All @@ -217,8 +237,12 @@ func (i IBCHandler) OnChanCloseConfirm(ctx sdk.Context, portID, channelID string
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", portID, channelID)
}

appVersion, err := i.versionDecoder.Decode(ctx, channelInfo.Version, portID, channelID)
if err != nil {
return err
}
msg := wasmvmtypes.IBCChannelCloseMsg{
CloseConfirm: &wasmvmtypes.IBCCloseConfirm{Channel: toWasmVMChannel(portID, channelID, channelInfo)},
CloseConfirm: &wasmvmtypes.IBCCloseConfirm{Channel: toWasmVMChannel(portID, channelID, channelInfo, appVersion)},
}
err = i.keeper.OnCloseChannel(ctx, contractAddr, msg)
if err != nil {
Expand All @@ -229,20 +253,12 @@ func (i IBCHandler) OnChanCloseConfirm(ctx sdk.Context, portID, channelID string
return err
}

func toWasmVMChannel(portID, channelID string, channelInfo channeltypes.Channel) wasmvmtypes.IBCChannel {
version := channelInfo.Version
if strings.TrimSpace(version) != "" {
// check for ics-29 middleware versions
var versionMetadata ibcfees.Metadata
if err := types.ModuleCdc.UnmarshalJSON([]byte(channelInfo.Version), &versionMetadata); err == nil {
version = versionMetadata.AppVersion
}
}
func toWasmVMChannel(portID, channelID string, channelInfo channeltypes.Channel, appVersion string) wasmvmtypes.IBCChannel {
return wasmvmtypes.IBCChannel{
Endpoint: wasmvmtypes.IBCEndpoint{PortID: portID, ChannelID: channelID},
CounterpartyEndpoint: wasmvmtypes.IBCEndpoint{PortID: channelInfo.Counterparty.PortId, ChannelID: channelInfo.Counterparty.ChannelId},
Order: channelInfo.Ordering.String(),
Version: version,
Version: appVersion,
ConnectionID: channelInfo.ConnectionHops[0], // At the moment this list must be of length 1. In the future multi-hop channels may be supported.
}
}
Expand Down
68 changes: 68 additions & 0 deletions x/wasm/ibc_middleware_support.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package wasm

import (
"encoding/json"
"strings"

sdk "github.com/cosmos/cosmos-sdk/types"
ibcfees "github.com/cosmos/ibc-go/v4/modules/apps/29-fee/types"
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"

"github.com/CosmWasm/wasmd/x/wasm/types"
)

var _ IBCAppVersionDecoder = AppVersionDecoderFn(nil)

// AppVersionDecoderFn custom type that implements IBCAppVersionDecoder
type AppVersionDecoderFn func(ctx sdk.Context, rawVersion, portID, channelID string) (string, error)

// Decode implements IBCAppVersionDecoder.Decode
func (a AppVersionDecoderFn) Decode(ctx sdk.Context, rawVersion, portID, channelID string) (string, error) {
return a(ctx, rawVersion, portID, channelID)
}

// AppVersionDecoderChain set up a chain of decoders where output of decoder n is feeded into n+1 as raw version
// until the last one is reached
func AppVersionDecoderChain(decoders ...IBCAppVersionDecoder) IBCAppVersionDecoder {
if len(decoders) == 0 {
panic("decoders must not be empty")
}
return AppVersionDecoderFn(func(ctx sdk.Context, rawVersion, portID, channelID string) (string, error) {
version := rawVersion
var err error
for _, d := range decoders {
version, err = d.Decode(ctx, version, portID, channelID)
if err != nil {
return "", err
}
}
return version, nil
})
}

// ICS29AppVersionDecoder decodes the ibc app version from an ics-29 fee middleware version when supported
// fallback is raw version when it can not be determined
func ICS29AppVersionDecoder(channelSource interface {
IsFeeEnabled(ctx sdk.Context, portID, channelID string) bool
},
) IBCAppVersionDecoder {
return AppVersionDecoderFn(func(ctx sdk.Context, rawVersion, portID, channelID string) (string, error) {
if channelSource.IsFeeEnabled(ctx, portID, channelID) {
Copy link
Member

Choose a reason for hiding this comment

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

If needed to get it to work, it is a hack and shows middleware stack is not complete.

There may be many new middleware and the wrapped applications shouldn't need to care, so no need to add eg ICS49 middleware that comes later

var meta ibcfees.Metadata
if err := types.ModuleCdc.UnmarshalJSON([]byte(rawVersion), &meta); err != nil {
return "", channeltypes.ErrInvalidChannelVersion
}
return meta.AppVersion, nil
}
if strings.TrimSpace(rawVersion) != "" && json.Valid([]byte(rawVersion)) {
// check for ics-29 middleware versions
var versionMetadata ibcfees.Metadata
if err := types.ModuleCdc.UnmarshalJSON([]byte(rawVersion), &versionMetadata); err == nil {
if strings.HasPrefix(versionMetadata.FeeVersion, "ics29-") { // sanity check
return versionMetadata.AppVersion, nil
}
}
}
return rawVersion, nil
})
}
103 changes: 103 additions & 0 deletions x/wasm/ibc_middleware_support_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package wasm

import (
"errors"
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAppVersionDecoderChain(t *testing.T) {
dropLastCharDec := AppVersionDecoderFn(func(_ sdk.Context, rawVersion, _, _ string) (string, error) {
return rawVersion[0 : len(rawVersion)-1], nil
})
alwaysErrDec := AppVersionDecoderFn(func(_ sdk.Context, rawVersion, _, _ string) (string, error) {
return "", errors.New("testing")
})
specs := map[string]struct {
dec IBCAppVersionDecoder
rawVersion string
expVersion string
expErr bool
}{
"single decoder": {
dec: AppVersionDecoderChain(ICS29AppVersionDecoder(IsFeeEnabledMock{true})),
rawVersion: `{"fee_version":"ics29-1", "app_version":"my version"}`,
expVersion: "my version",
},
"multiple decoders": {
dec: AppVersionDecoderChain(ICS29AppVersionDecoder(IsFeeEnabledMock{true}), dropLastCharDec, dropLastCharDec),
rawVersion: `{"fee_version":"ics29-1", "app_version":"123"}`,
expVersion: "1",
},
"multiple decoders with err": {
dec: AppVersionDecoderChain(ICS29AppVersionDecoder(IsFeeEnabledMock{true}), alwaysErrDec, dropLastCharDec),
expErr: true,
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
gotVersion, gotErr := spec.dec.Decode(sdk.Context{}, spec.rawVersion, "foo", "bar")
if spec.expErr {
require.Error(t, gotErr)
return
}
require.NoError(t, gotErr)
assert.Equal(t, spec.expVersion, gotVersion)
})
}
}

func TestICS29AppVersionDecoder(t *testing.T) {
specs := map[string]struct {
rawVersion string
isFeeChannel bool
expVersion string
expErr bool
}{
"raw version": {
rawVersion: "my version",
expVersion: "my version",
},
"ics29 version on fee channel": {
rawVersion: `{"fee_version":"ics29-1", "app_version":"my version"}`,
isFeeChannel: true,
expVersion: "my version",
},
"invalid ics29 version on fee channel": {
rawVersion: `not-a-json-string`,
isFeeChannel: true,
expErr: true,
},
"ics29 version on non fee channel": {
rawVersion: `{"fee_version":"ics29-1", "app_version":"my version"}`,
expVersion: "my version",
},
"non ics29 version on non fee channel": {
rawVersion: `{"fee_version":"alx29-1", "app_version":"my version"}`,
expVersion: `{"fee_version":"alx29-1", "app_version":"my version"}`,
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
gotVersion, gotErr := ICS29AppVersionDecoder(IsFeeEnabledMock{spec.isFeeChannel}).
Decode(sdk.Context{}, spec.rawVersion, "foo", "bar")
if spec.expErr {
require.Error(t, gotErr)
return
}
require.NoError(t, gotErr)
assert.Equal(t, spec.expVersion, gotVersion)
})
}
}

type IsFeeEnabledMock struct {
result bool
}

func (f IsFeeEnabledMock) IsFeeEnabled(_ sdk.Context, _, _ string) bool {
return f.result
}