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

IBC Callbacks #1817

Merged
merged 20 commits into from
Jul 4, 2024
Merged
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
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)
chipshort marked this conversation as resolved.
Show resolved Hide resolved

// 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
chipshort marked this conversation as resolved.
Show resolved Hide resolved
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) {
chipshort marked this conversation as resolved.
Show resolved Hide resolved
// 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