Skip to content

Commit f360693

Browse files
committed
feat: IBCv2 timeout handler
1 parent 22b96eb commit f360693

File tree

9 files changed

+167
-4
lines changed

9 files changed

+167
-4
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/CosmWasm/wasmd
33
go 1.23.6
44

55
require (
6-
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250418110315-37c6c8e22194
6+
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250424105703-4958b470975c
77
github.com/cosmos/cosmos-proto v1.0.0-beta.5
88
github.com/cosmos/cosmos-sdk v0.50.12
99
github.com/cosmos/gogogateway v1.2.0 // indirect

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
227227
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
228228
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
229229
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
230-
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250418110315-37c6c8e22194 h1:RN8oHsXrOYpQSlbxW90MhwpMEkC4TxbmxvHSoQsxT+Q=
231-
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250418110315-37c6c8e22194/go.mod h1:Aj/rB2KMRM8nAdbWxkO23rnQYb5KsoPuH9ZizSi0sVg=
230+
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250424105703-4958b470975c h1:xdnYXLc9ql4xO64ZaBSRT5Q6n55FedB4qkb1f09dN/w=
231+
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250424105703-4958b470975c/go.mod h1:Aj/rB2KMRM8nAdbWxkO23rnQYb5KsoPuH9ZizSi0sVg=
232232
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
233233
github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q=
234234
github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=

tests/e2e/ibc2_test.go

+58-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type QueryMsg struct {
2323
// ibc2 contract response type
2424
type State struct {
2525
IBC2PacketReceiveCounter uint32 `json:"ibc2_packet_receive_counter"`
26+
IBC2PacketTimeoutCounter uint32 `json:"ibc2_packet_timeout_counter"`
2627
LastChannelID string `json:"last_channel_id"`
2728
LastPacketSeq uint64 `json:"last_packet_seq"`
2829
}
@@ -33,7 +34,7 @@ type IbcPayload struct {
3334
SendAsyncAckForPrevMsg bool `json:"send_async_ack_for_prev_msg"`
3435
}
3536

36-
func TestIBC2SendMsg(t *testing.T) {
37+
func TestIBC2SendReceiveMsg(t *testing.T) {
3738
coord := wasmibctesting.NewCoordinator(t, 2)
3839
chainA := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(1)))
3940
chainB := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(2)))
@@ -105,3 +106,59 @@ func TestIBC2SendMsg(t *testing.T) {
105106
require.Equal(t, uint32(i), response.IBC2PacketReceiveCounter)
106107
}
107108
}
109+
110+
func TestIBC2TimeoutMsg(t *testing.T) {
111+
coord := wasmibctesting.NewCoordinator(t, 2)
112+
chainA := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(1)))
113+
chainB := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(2)))
114+
contractCodeA := chainA.StoreCodeFile("./testdata/ibc2.wasm").CodeID
115+
contractAddrA := chainA.InstantiateContract(contractCodeA, []byte(`{}`))
116+
contractPortA := wasmkeeper.PortIDForContractV2(contractAddrA)
117+
require.NotEmpty(t, contractAddrA)
118+
119+
contractCodeB := chainB.StoreCodeFile("./testdata/ibc2.wasm").CodeID
120+
// Skip initial contract address to not overlap with ChainA
121+
_ = chainB.InstantiateContract(contractCodeB, []byte(`{}`))
122+
contractAddrB := chainB.InstantiateContract(contractCodeB, []byte(`{}`))
123+
contractPortB := wasmkeeper.PortIDForContractV2(contractAddrB)
124+
require.NotEmpty(t, contractAddrB)
125+
126+
path := wasmibctesting.NewWasmPath(chainA, chainB)
127+
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
128+
PortID: contractPortA,
129+
Version: ibctransfertypes.V1,
130+
Order: channeltypes.UNORDERED,
131+
}
132+
path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{
133+
PortID: contractPortB,
134+
Version: ibctransfertypes.V1,
135+
Order: channeltypes.UNORDERED,
136+
}
137+
138+
path.Path.SetupV2()
139+
140+
// IBC v2 Payload from contract on Chain B to contract on Chain A
141+
payload := mockv2.NewMockPayload(contractPortB, contractPortA)
142+
var err error
143+
payload.Value, err = json.Marshal(IbcPayload{ResponseWithoutAck: false, SendAsyncAckForPrevMsg: false})
144+
require.NoError(t, err)
145+
146+
// Message timeout
147+
timeoutTimestamp := uint64(chainB.GetContext().BlockTime().Add(time.Minute).Unix())
148+
149+
_, err = path.EndpointB.MsgSendPacket(timeoutTimestamp, payload)
150+
require.NoError(t, err)
151+
152+
// First message send through test
153+
err = wasmibctesting.RelayPendingPacketsV2(path)
154+
require.NoError(t, err)
155+
156+
// Check that timeout message is sent to the contract
157+
err = wasmibctesting.TimeoutPendingPacketsV2(coord, path)
158+
require.NoError(t, err)
159+
160+
var response State
161+
err = chainA.SmartQuery(contractAddrA.String(), QueryMsg{QueryState: struct{}{}}, &response)
162+
require.NoError(t, err)
163+
require.Equal(t, uint32(1), response.IBC2PacketTimeoutCounter)
164+
}

tests/e2e/testdata/ibc2.wasm

6.75 KB
Binary file not shown.

tests/wasmibctesting/utils.go

+29
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types"
2222
channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types"
2323
host "github.com/cosmos/ibc-go/v10/modules/core/24-host"
24+
hostv2 "github.com/cosmos/ibc-go/v10/modules/core/24-host/v2"
2425
ibctesting "github.com/cosmos/ibc-go/v10/testing"
2526
"github.com/stretchr/testify/require"
2627

@@ -495,6 +496,34 @@ func TimeoutPendingPackets(coord *ibctesting.Coordinator, path *WasmPath) error
495496
return nil
496497
}
497498

499+
// TimeoutPendingPackets returns the package to source chain to let the IBC app revert any operation.
500+
// from A to B
501+
func TimeoutPendingPacketsV2(coord *ibctesting.Coordinator, path *WasmPath) error {
502+
src := path.EndpointA
503+
dest := path.EndpointB
504+
505+
toSend := path.chainA.PendingSendPacketsV2
506+
coord.Logf("Timeout %d Packets A->B\n", len(*toSend))
507+
require.NoError(coord, src.UpdateClient())
508+
509+
// Increment time and commit block so that 1 minute delay period passes between send and receive
510+
coord.IncrementTimeBy(time.Minute)
511+
err := path.EndpointA.UpdateClient()
512+
require.NoError(coord, err)
513+
for _, packet := range *toSend {
514+
// get proof of packet unreceived on dest
515+
packetKey := hostv2.PacketReceiptKey(packet.GetDestinationClient(), packet.GetSequence())
516+
proofUnreceived, proofHeight := dest.QueryProof(packetKey)
517+
timeoutMsg := channeltypesv2.NewMsgTimeout(packet, proofUnreceived, proofHeight, src.Chain.SenderAccount.GetAddress().String())
518+
_, err := path.chainA.SendMsgs(timeoutMsg)
519+
if err != nil {
520+
return err
521+
}
522+
}
523+
*path.chainA.PendingSendPackets = []channeltypes.Packet{}
524+
return nil
525+
}
526+
498527
// CloseChannel close channel on both sides
499528
func CloseChannel(coord *ibctesting.Coordinator, path *ibctesting.Path) {
500529
err := path.EndpointA.ChanCloseInit()

x/wasm/keeper/ibc2.go

+50
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ func (module IBC2Handler) OnTimeoutPacket(
7373
payload channeltypesv2.Payload,
7474
relayer sdk.AccAddress,
7575
) error {
76+
contractAddr, err := ContractFromPortID2(payload.SourcePort)
77+
if err != nil {
78+
return errorsmod.Wrapf(err, "contract port id")
79+
}
80+
msg := wasmvmtypes.IBC2PacketTimeoutMsg{
81+
Payload: newIBC2Payload(payload),
82+
SourceClient: sourceClient,
83+
DestinationClient: destinationClient,
84+
PacketSequence: sequence,
85+
Relayer: relayer.String(),
86+
}
87+
err = module.keeper.OnTimeoutIBC2Packet(ctx, contractAddr, msg)
88+
if err != nil {
89+
return errorsmod.Wrap(err, "on timeout")
90+
}
7691
return nil
7792
}
7893

@@ -165,6 +180,41 @@ func (k Keeper) OnRecvIBC2Packet(
165180
}
166181
}
167182

183+
// OnTimeoutPacket calls the contract to let it know the packet was never received on the destination chain within
184+
// the timeout boundaries.
185+
// The contract should handle this on the application level and undo the original operation
186+
func (k Keeper) OnTimeoutIBC2Packet(
187+
ctx sdk.Context,
188+
contractAddr sdk.AccAddress,
189+
msg wasmvmtypes.IBC2PacketTimeoutMsg,
190+
) error {
191+
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-timeout-packet")
192+
193+
contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr)
194+
if err != nil {
195+
return err
196+
}
197+
198+
env := types.NewEnv(ctx, contractAddr)
199+
querier := k.newQueryHandler(ctx, contractAddr)
200+
201+
gasLeft := k.runtimeGasForContract(ctx)
202+
res, gasUsed, execErr := k.wasmVM.IBC2PacketTimeout(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gasLeft, costJSONDeserialization)
203+
k.consumeRuntimeGas(ctx, gasUsed)
204+
if execErr != nil {
205+
return errorsmod.Wrap(types.ErrExecuteFailed, execErr.Error())
206+
}
207+
if res == nil {
208+
// If this gets executed, that's a bug in wasmvm
209+
return errorsmod.Wrap(types.ErrVMError, "internal wasmvm error")
210+
}
211+
if res.Err != "" {
212+
return types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrExecuteFailed, res.Err))
213+
}
214+
215+
return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res.Ok)
216+
}
217+
168218
func newIBC2Payload(payload channeltypesv2.Payload) wasmvmtypes.IBC2Payload {
169219
return wasmvmtypes.IBC2Payload{
170220
SourcePort: payload.SourcePort,

x/wasm/keeper/wasmtesting/mock_engine.go

+8
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type MockWasmEngine struct {
4444
IBCSourceCallbackFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCSourceCallbackMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResult, uint64, error)
4545
IBCDestinationCallbackFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBCDestinationCallbackMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResult, uint64, error)
4646
IBC2PacketReceiveFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBC2PacketReceiveMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCReceiveResult, uint64, error)
47+
IBC2PacketTimeoutFn func(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBC2PacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResult, uint64, error)
4748
PinFn func(checksum wasmvm.Checksum) error
4849
UnpinFn func(checksum wasmvm.Checksum) error
4950
GetMetricsFn func() (*wasmvmtypes.Metrics, error)
@@ -113,6 +114,13 @@ func (m *MockWasmEngine) IBC2PacketReceive(codeID wasmvm.Checksum, env wasmvmtyp
113114
return m.IBC2PacketReceiveFn(codeID, env, msg, store, goapi, querier, gasMeter, gasLimit, deserCost)
114115
}
115116

117+
func (m *MockWasmEngine) IBC2PacketTimeout(codeID wasmvm.Checksum, env wasmvmtypes.Env, msg wasmvmtypes.IBC2PacketTimeoutMsg, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.IBCBasicResult, uint64, error) {
118+
if m.IBC2PacketTimeoutFn == nil {
119+
panic("not supposed to be called!")
120+
}
121+
return m.IBC2PacketTimeoutFn(codeID, env, msg, store, goapi, querier, gasMeter, gasLimit, deserCost)
122+
}
123+
116124
func (m *MockWasmEngine) StoreCode(codeID wasmvm.WasmCode, gasLimit uint64) (wasmvm.Checksum, uint64, error) {
117125
if m.StoreCodeFn == nil {
118126
panic("not supposed to be called!")

x/wasm/types/exported_keepers.go

+5
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,9 @@ type IBC2ContractKeeper interface {
146146
contractAddr sdk.AccAddress,
147147
msg wasmvmtypes.IBC2PacketReceiveMsg,
148148
) channeltypesv2.RecvPacketResult
149+
OnTimeoutIBC2Packet(
150+
ctx sdk.Context,
151+
contractAddr sdk.AccAddress,
152+
msg wasmvmtypes.IBC2PacketTimeoutMsg,
153+
) error
149154
}

x/wasm/types/wasmer_engine.go

+14
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,20 @@ type WasmEngine interface {
306306
deserCost wasmvmtypes.UFraction,
307307
) (*wasmvmtypes.IBCReceiveResult, uint64, error)
308308

309+
// IBC2PacketTimeout is available on IBCv2-enabled contracts and is called when an
310+
// outgoing packet (previously sent by this contract) will probably never be executed.
311+
IBC2PacketTimeout(
312+
checksum wasmvm.Checksum,
313+
env wasmvmtypes.Env,
314+
packet wasmvmtypes.IBC2PacketTimeoutMsg,
315+
store wasmvm.KVStore,
316+
goapi wasmvm.GoAPI,
317+
querier wasmvm.Querier,
318+
gasMeter wasmvm.GasMeter,
319+
gasLimit uint64,
320+
deserCost wasmvmtypes.UFraction,
321+
) (*wasmvmtypes.IBCBasicResult, uint64, error)
322+
309323
// Pin pins a code to an in-memory cache, such that is
310324
// always loaded quickly when executed.
311325
// Pin is idempotent.

0 commit comments

Comments
 (0)