Skip to content

Commit

Permalink
Merge pull request #1817 from CosmWasm/ibc-callbacks
Browse files Browse the repository at this point in the history
IBC Callbacks
  • Loading branch information
chipshort authored Jul 4, 2024
2 parents 74b5871 + 98b91e7 commit 5657a01
Show file tree
Hide file tree
Showing 11 changed files with 533 additions and 39 deletions.
27 changes: 19 additions & 8 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
dbm "github.com/cosmos/cosmos-db"
"github.com/cosmos/gogoproto/proto"
ibccallbacks "github.com/cosmos/ibc-go/modules/apps/callbacks"
"github.com/cosmos/ibc-go/modules/capability"
capabilitykeeper "github.com/cosmos/ibc-go/modules/capability/keeper"
capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types"
Expand Down Expand Up @@ -650,10 +651,10 @@ func NewWasmApp(
wasmOpts...,
)

// Create Transfer Stack
var transferStack porttypes.IBCModule
transferStack = transfer.NewIBCModule(app.TransferKeeper)
transferStack = ibcfee.NewIBCMiddleware(transferStack, app.IBCFeeKeeper)
// Create fee enabled wasm ibc Stack
var wasmStack porttypes.IBCModule
wasmStackIBCHandler := wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper, app.IBCFeeKeeper)
wasmStack = ibcfee.NewIBCMiddleware(wasmStackIBCHandler, app.IBCFeeKeeper)

// Create Interchain Accounts Stack
// SendPacket, since it is originating from the application to core IBC:
Expand All @@ -663,18 +664,28 @@ func NewWasmApp(
// see https://medium.com/the-interchain-foundation/ibc-go-v6-changes-to-interchain-accounts-and-how-it-impacts-your-chain-806c185300d7
var noAuthzModule porttypes.IBCModule
icaControllerStack = icacontroller.NewIBCMiddleware(noAuthzModule, app.ICAControllerKeeper)
// app.ICAAuthModule = icaControllerStack.(ibcmock.IBCModule)
icaControllerStack = icacontroller.NewIBCMiddleware(icaControllerStack, app.ICAControllerKeeper)
icaControllerStack = ibccallbacks.NewIBCMiddleware(icaControllerStack, app.IBCFeeKeeper, wasmStackIBCHandler, wasm.DefaultMaxIBCCallbackGas)
icaICS4Wrapper := icaControllerStack.(porttypes.ICS4Wrapper)
icaControllerStack = ibcfee.NewIBCMiddleware(icaControllerStack, app.IBCFeeKeeper)
// Since the callbacks middleware itself is an ics4wrapper, it needs to be passed to the ica controller keeper
app.ICAControllerKeeper.WithICS4Wrapper(icaICS4Wrapper)

// RecvPacket, message that originates from core IBC and goes down to app, the flow is:
// channel.RecvPacket -> fee.OnRecvPacket -> icaHost.OnRecvPacket
var icaHostStack porttypes.IBCModule
icaHostStack = icahost.NewIBCModule(app.ICAHostKeeper)
icaHostStack = ibcfee.NewIBCMiddleware(icaHostStack, app.IBCFeeKeeper)

// Create fee enabled wasm ibc Stack
var wasmStack porttypes.IBCModule
wasmStack = wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper, app.IBCFeeKeeper)
wasmStack = ibcfee.NewIBCMiddleware(wasmStack, app.IBCFeeKeeper)
// Create Transfer Stack
var transferStack porttypes.IBCModule
transferStack = transfer.NewIBCModule(app.TransferKeeper)
transferStack = ibccallbacks.NewIBCMiddleware(transferStack, app.IBCFeeKeeper, wasmStackIBCHandler, wasm.DefaultMaxIBCCallbackGas)
transferICS4Wrapper := transferStack.(porttypes.ICS4Wrapper)
transferStack = ibcfee.NewIBCMiddleware(transferStack, app.IBCFeeKeeper)
// Since the callbacks middleware itself is an ics4wrapper, it needs to be passed to the ica controller keeper
app.TransferKeeper.WithICS4Wrapper(transferICS4Wrapper)

// Create static IBC router, add app routes, then set and seal it
ibcRouter := porttypes.NewRouter().
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require (
cosmossdk.io/x/upgrade v0.1.3
github.com/cometbft/cometbft v0.38.9
github.com/cosmos/cosmos-db v1.0.2
github.com/cosmos/ibc-go/modules/apps/callbacks v0.2.1-0.20231113120333-342c00b0f8bd
github.com/cosmos/ibc-go/modules/capability v1.0.0
github.com/cosmos/ibc-go/v8 v8.3.2
github.com/distribution/reference v0.5.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,8 @@ github.com/cosmos/gogoproto v1.5.0 h1:SDVwzEqZDDBoslaeZg+dGE55hdzHfgUA40pEanMh52
github.com/cosmos/gogoproto v1.5.0/go.mod h1:iUM31aofn3ymidYG6bUR5ZFrk+Om8p5s754eMUcyp8I=
github.com/cosmos/iavl v1.2.0 h1:kVxTmjTh4k0Dh1VNL046v6BXqKziqMDzxo93oh3kOfM=
github.com/cosmos/iavl v1.2.0/go.mod h1:HidWWLVAtODJqFD6Hbne2Y0q3SdxByJepHUOeoH4LiI=
github.com/cosmos/ibc-go/modules/apps/callbacks v0.2.1-0.20231113120333-342c00b0f8bd h1:Lx+/5dZ/nN6qPXP2Ofog6u1fmlkCFA1ElcOconnofEM=
github.com/cosmos/ibc-go/modules/apps/callbacks v0.2.1-0.20231113120333-342c00b0f8bd/go.mod h1:JWfpWVKJKiKtd53/KbRoKfxWl8FsT2GPcNezTOk0o5Q=
github.com/cosmos/ibc-go/modules/capability v1.0.0 h1:r/l++byFtn7jHYa09zlAdSeevo8ci1mVZNO9+V0xsLE=
github.com/cosmos/ibc-go/modules/capability v1.0.0/go.mod h1:D81ZxzjZAe0ZO5ambnvn1qedsFQ8lOwtqicG6liLBco=
github.com/cosmos/ibc-go/v8 v8.3.2 h1:8X1oHHKt2Bh9hcExWS89rntLaCKZp2EjFTUSxKlPhGI=
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# End To End Testing - e2e

Scenario tests that run against on or multiple chain instances.
Scenario tests that run against one or multiple chain instances.
225 changes: 225 additions & 0 deletions tests/e2e/ibc_callbacks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package e2e_test

import (
"encoding/json"
"fmt"
"testing"
"time"

wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types"
ibcfee "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types"
ibctransfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
ibctesting "github.com/cosmos/ibc-go/v8/testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

sdkmath "cosmossdk.io/math"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/CosmWasm/wasmd/app"
"github.com/CosmWasm/wasmd/tests/e2e"
wasmibctesting "github.com/CosmWasm/wasmd/x/wasm/ibctesting"
"github.com/CosmWasm/wasmd/x/wasm/types"
)

func TestIBCCallbacks(t *testing.T) {
// scenario:
// given two chains
// with an ics-20 channel established
// and an ibc-callbacks contract deployed on chain A and B each
// when the contract on A sends an IBCMsg::Transfer to the contract on B
// then the contract on B should receive a destination chain callback
// and the contract on A should receive a source chain callback with the result (ack or timeout)
marshaler := app.MakeEncodingConfig(t).Codec
coord := wasmibctesting.NewCoordinator(t, 2)
chainA := coord.GetChain(wasmibctesting.GetChainID(1))
chainB := coord.GetChain(wasmibctesting.GetChainID(2))

actorChainA := sdk.AccAddress(chainA.SenderPrivKey.PubKey().Address())
oneToken := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(1)))

path := wasmibctesting.NewPath(chainA, chainB)
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
PortID: ibctransfertypes.PortID,
Version: string(marshaler.MustMarshalJSON(&ibcfee.Metadata{FeeVersion: ibcfee.Version, AppVersion: ibctransfertypes.Version})),
Order: channeltypes.UNORDERED,
}
path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{
PortID: ibctransfertypes.PortID,
Version: string(marshaler.MustMarshalJSON(&ibcfee.Metadata{FeeVersion: ibcfee.Version, AppVersion: ibctransfertypes.Version})),
Order: channeltypes.UNORDERED,
}
// with an ics-20 transfer channel setup between both chains
coord.Setup(path)

// with an ibc-callbacks contract deployed on chain A
codeIDonA := chainA.StoreCodeFile("./testdata/ibc_callbacks.wasm").CodeID

// and on chain B
codeIDonB := chainB.StoreCodeFile("./testdata/ibc_callbacks.wasm").CodeID

type TransferExecMsg struct {
ToAddress string `json:"to_address"`
ChannelID string `json:"channel_id"`
TimeoutSeconds uint32 `json:"timeout_seconds"`
}
// ExecuteMsg is the ibc-callbacks contract's execute msg
type ExecuteMsg struct {
Transfer *TransferExecMsg `json:"transfer"`
}
type QueryMsg struct {
CallbackStats struct{} `json:"callback_stats"`
}
type QueryResp struct {
IBCAckCallbacks []wasmvmtypes.IBCPacketAckMsg `json:"ibc_ack_callbacks"`
IBCTimeoutCallbacks []wasmvmtypes.IBCPacketTimeoutMsg `json:"ibc_timeout_callbacks"`
IBCDestinationCallbacks []wasmvmtypes.IBCDestinationCallbackMsg `json:"ibc_destination_callbacks"`
}

specs := map[string]struct {
contractMsg ExecuteMsg
// expAck is true if the packet is relayed, false if it times out
expAck bool
}{
"success": {
contractMsg: ExecuteMsg{
Transfer: &TransferExecMsg{
ChannelID: path.EndpointA.ChannelID,
TimeoutSeconds: 100,
},
},
expAck: true,
},
"timeout": {
contractMsg: ExecuteMsg{
Transfer: &TransferExecMsg{
ChannelID: path.EndpointA.ChannelID,
TimeoutSeconds: 1,
},
},
expAck: false,
},
}

for name, spec := range specs {
t.Run(name, func(t *testing.T) {
contractAddrA := chainA.InstantiateContract(codeIDonA, []byte(`{}`))
require.NotEmpty(t, contractAddrA)
contractAddrB := chainB.InstantiateContract(codeIDonB, []byte(`{}`))
require.NotEmpty(t, contractAddrB)

if spec.contractMsg.Transfer != nil && spec.contractMsg.Transfer.ToAddress == "" {
spec.contractMsg.Transfer.ToAddress = contractAddrB.String()
}
contractMsgBz, err := json.Marshal(spec.contractMsg)
require.NoError(t, err)

// when the contract on chain A sends an IBCMsg::Transfer to the contract on chain B
execMsg := types.MsgExecuteContract{
Sender: actorChainA.String(),
Contract: contractAddrA.String(),
Msg: contractMsgBz,
Funds: oneToken,
}
_, err = chainA.SendMsgs(&execMsg)
require.NoError(t, err)

if spec.expAck {
// and the packet is relayed
require.NoError(t, coord.RelayAndAckPendingPackets(path))

// then the contract on chain B should receive a receive callback
var response QueryResp
chainB.SmartQuery(contractAddrB.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
assert.Empty(t, response.IBCAckCallbacks)
assert.Empty(t, response.IBCTimeoutCallbacks)
assert.Len(t, response.IBCDestinationCallbacks, 1)

// and the receive callback should contain the ack
assert.Equal(t, []byte("{\"result\":\"AQ==\"}"), response.IBCDestinationCallbacks[0].Ack.Data)

// and the contract on chain A should receive a callback with the ack
chainA.SmartQuery(contractAddrA.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
assert.Len(t, response.IBCAckCallbacks, 1)
assert.Empty(t, response.IBCTimeoutCallbacks)
assert.Empty(t, response.IBCDestinationCallbacks)

// and the ack result should be the ics20 success ack
assert.Equal(t, []byte(`{"result":"AQ=="}`), response.IBCAckCallbacks[0].Acknowledgement.Data)
} else {
// and the packet times out
require.NoError(t, coord.TimeoutPendingPackets(path))

// then the contract on chain B should not receive anything
var response QueryResp
chainB.SmartQuery(contractAddrB.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
assert.Empty(t, response.IBCAckCallbacks)
assert.Empty(t, response.IBCTimeoutCallbacks)
assert.Empty(t, response.IBCDestinationCallbacks)

// and the contract on chain A should receive a callback with the timeout result
chainA.SmartQuery(contractAddrA.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
assert.Empty(t, response.IBCAckCallbacks)
assert.Len(t, response.IBCTimeoutCallbacks, 1)
assert.Empty(t, response.IBCDestinationCallbacks)
}
})
}
}

func TestIBCCallbacksWithoutEntrypoints(t *testing.T) {
// scenario:
// given two chains
// with an ics-20 channel established
// and a reflect contract deployed on chain A and B each
// when the contract on A sends an IBCMsg::Transfer to the contract on B
// then the VM should try to call the callback on B and fail gracefully
// and should try to call the callback on A and fail gracefully
marshaler := app.MakeEncodingConfig(t).Codec
coord := wasmibctesting.NewCoordinator(t, 2)
chainA := coord.GetChain(wasmibctesting.GetChainID(1))
chainB := coord.GetChain(wasmibctesting.GetChainID(2))

oneToken := sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(1))

path := wasmibctesting.NewPath(chainA, chainB)
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
PortID: ibctransfertypes.PortID,
Version: string(marshaler.MustMarshalJSON(&ibcfee.Metadata{FeeVersion: ibcfee.Version, AppVersion: ibctransfertypes.Version})),
Order: channeltypes.UNORDERED,
}
path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{
PortID: ibctransfertypes.PortID,
Version: string(marshaler.MustMarshalJSON(&ibcfee.Metadata{FeeVersion: ibcfee.Version, AppVersion: ibctransfertypes.Version})),
Order: channeltypes.UNORDERED,
}
// with an ics-20 transfer channel setup between both chains
coord.Setup(path)

// with a reflect contract deployed on chain A and B
contractAddrA := e2e.InstantiateReflectContract(t, chainA)
chainA.Fund(contractAddrA, oneToken.Amount)
contractAddrB := e2e.InstantiateReflectContract(t, chainA)

// when the contract on A sends an IBCMsg::Transfer to the contract on B
memo := fmt.Sprintf(`{"src_callback":{"address":"%v"},"dest_callback":{"address":"%v"}}`, contractAddrA.String(), contractAddrB.String())
e2e.MustExecViaReflectContract(t, chainA, contractAddrA, wasmvmtypes.CosmosMsg{
IBC: &wasmvmtypes.IBCMsg{
Transfer: &wasmvmtypes.TransferMsg{
ToAddress: contractAddrB.String(),
ChannelID: path.EndpointA.ChannelID,
Amount: wasmvmtypes.NewCoin(oneToken.Amount.Uint64(), oneToken.Denom),
Timeout: wasmvmtypes.IBCTimeout{
Timestamp: uint64(chainA.LastHeader.GetTime().Add(time.Second * 100).UnixNano()),
},
Memo: memo,
},
},
})

// and the packet is relayed without problems
require.NoError(t, coord.RelayAndAckPendingPackets(path))
assert.Empty(t, chainA.PendingSendPackets)
}
Binary file added tests/e2e/testdata/ibc_callbacks.wasm
Binary file not shown.
Loading

0 comments on commit 5657a01

Please sign in to comment.