Skip to content

feat: IBCv2 timeout handler #2226

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

Merged
merged 1 commit into from
Apr 29, 2025
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/CosmWasm/wasmd
go 1.23.6

require (
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250418110315-37c6c8e22194
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250429110632-913b1c3f2217
github.com/cosmos/cosmos-proto v1.0.0-beta.5
github.com/cosmos/cosmos-sdk v0.50.12
github.com/cosmos/gogogateway v1.2.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250418110315-37c6c8e22194 h1:RN8oHsXrOYpQSlbxW90MhwpMEkC4TxbmxvHSoQsxT+Q=
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250418110315-37c6c8e22194/go.mod h1:Aj/rB2KMRM8nAdbWxkO23rnQYb5KsoPuH9ZizSi0sVg=
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250429110632-913b1c3f2217 h1:9ZBqH4H5aBDywINoSW/r3nIymbxS2NnM2cMcinF+1Y8=
github.com/CosmWasm/wasmvm/v2 v2.2.2-0.20250429110632-913b1c3f2217/go.mod h1:Aj/rB2KMRM8nAdbWxkO23rnQYb5KsoPuH9ZizSi0sVg=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q=
github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
Expand Down
61 changes: 60 additions & 1 deletion tests/e2e/ibc2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type QueryMsg struct {
// ibc2 contract response type
type State struct {
IBC2PacketReceiveCounter uint32 `json:"ibc2_packet_receive_counter"`
IBC2PacketTimeoutCounter uint32 `json:"ibc2_packet_timeout_counter"`
LastChannelID string `json:"last_channel_id"`
LastPacketSeq uint64 `json:"last_packet_seq"`
}
Expand All @@ -33,7 +34,7 @@ type IbcPayload struct {
SendAsyncAckForPrevMsg bool `json:"send_async_ack_for_prev_msg"`
}

func TestIBC2SendMsg(t *testing.T) {
func TestIBC2SendReceiveMsg(t *testing.T) {
coord := wasmibctesting.NewCoordinator(t, 2)
chainA := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(1)))
chainB := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(2)))
Expand Down Expand Up @@ -105,3 +106,61 @@ func TestIBC2SendMsg(t *testing.T) {
require.Equal(t, uint32(i), response.IBC2PacketReceiveCounter)
}
}

func TestIBC2TimeoutMsg(t *testing.T) {
coord := wasmibctesting.NewCoordinator(t, 2)
chainA := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(1)))
chainB := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(2)))
contractCodeA := chainA.StoreCodeFile("./testdata/ibc2.wasm").CodeID
contractAddrA := chainA.InstantiateContract(contractCodeA, []byte(`{}`))
contractPortA := wasmkeeper.PortIDForContractV2(contractAddrA)
require.NotEmpty(t, contractAddrA)

contractCodeB := chainB.StoreCodeFile("./testdata/ibc2.wasm").CodeID
// Skip initial contract address to not overlap with ChainA
_ = chainB.InstantiateContract(contractCodeB, []byte(`{}`))
contractAddrB := chainB.InstantiateContract(contractCodeB, []byte(`{}`))
contractPortB := wasmkeeper.PortIDForContractV2(contractAddrB)
require.NotEmpty(t, contractAddrB)

path := wasmibctesting.NewWasmPath(chainA, chainB)
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
PortID: contractPortA,
Version: ibctransfertypes.V1,
Order: channeltypes.UNORDERED,
}
path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{
PortID: contractPortB,
Version: ibctransfertypes.V1,
Order: channeltypes.UNORDERED,
}

path.Path.SetupV2()

// IBC v2 Payload from contract on Chain B to contract on Chain A
payload := mockv2.NewMockPayload(contractPortB, contractPortA)
var err error
payload.Value, err = json.Marshal(IbcPayload{ResponseWithoutAck: false, SendAsyncAckForPrevMsg: false})
require.NoError(t, err)

// Message timeout
timeoutTimestamp := uint64(chainB.GetContext().BlockTime().Add(time.Minute).Unix())

_, err = path.EndpointB.MsgSendPacket(timeoutTimestamp, payload)
require.NoError(t, err)

// First message send through test.
// Contract replies back with the IbcPayload response.
err = wasmibctesting.RelayPendingPacketsV2(path)
require.NoError(t, err)

// Send timeout on the packet sent by the contract
err = wasmibctesting.TimeoutPendingPacketsV2(coord, path)
require.NoError(t, err)

// Check that timeout message was sent to the contract
var response State
err = chainA.SmartQuery(contractAddrA.String(), QueryMsg{QueryState: struct{}{}}, &response)
require.NoError(t, err)
require.Equal(t, uint32(1), response.IBC2PacketTimeoutCounter)
}
Binary file modified tests/e2e/testdata/ibc2.wasm
Binary file not shown.
29 changes: 29 additions & 0 deletions tests/wasmibctesting/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types"
channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types"
host "github.com/cosmos/ibc-go/v10/modules/core/24-host"
hostv2 "github.com/cosmos/ibc-go/v10/modules/core/24-host/v2"
ibctesting "github.com/cosmos/ibc-go/v10/testing"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -495,6 +496,34 @@
return nil
}

// TimeoutPendingPacketsV2 returns the package to source chain to let the IBCv2 app revert any operation.
// from A to B
func TimeoutPendingPacketsV2(coord *ibctesting.Coordinator, path *WasmPath) error {
src := path.EndpointA
dest := path.EndpointB

Check warning on line 503 in tests/wasmibctesting/utils.go

View check run for this annotation

Codecov / codecov/patch

tests/wasmibctesting/utils.go#L501-L503

Added lines #L501 - L503 were not covered by tests

toSend := path.chainA.PendingSendPacketsV2
coord.Logf("Timeout %d Packets A->B\n", len(*toSend))
require.NoError(coord, src.UpdateClient())

Check warning on line 507 in tests/wasmibctesting/utils.go

View check run for this annotation

Codecov / codecov/patch

tests/wasmibctesting/utils.go#L505-L507

Added lines #L505 - L507 were not covered by tests

// Increment time and commit block so that 1 minute delay period passes between send and receive
coord.IncrementTimeBy(time.Minute)
err := path.EndpointA.UpdateClient()
require.NoError(coord, err)
for _, packet := range *toSend {

Check warning on line 513 in tests/wasmibctesting/utils.go

View check run for this annotation

Codecov / codecov/patch

tests/wasmibctesting/utils.go#L510-L513

Added lines #L510 - L513 were not covered by tests
// get proof of packet unreceived on dest
packetKey := hostv2.PacketReceiptKey(packet.GetDestinationClient(), packet.GetSequence())
proofUnreceived, proofHeight := dest.QueryProof(packetKey)
timeoutMsg := channeltypesv2.NewMsgTimeout(packet, proofUnreceived, proofHeight, src.Chain.SenderAccount.GetAddress().String())
_, err := path.chainA.SendMsgs(timeoutMsg)
if err != nil {
return err

Check warning on line 520 in tests/wasmibctesting/utils.go

View check run for this annotation

Codecov / codecov/patch

tests/wasmibctesting/utils.go#L515-L520

Added lines #L515 - L520 were not covered by tests
}
}
*path.chainA.PendingSendPackets = []channeltypes.Packet{}
return nil

Check warning on line 524 in tests/wasmibctesting/utils.go

View check run for this annotation

Codecov / codecov/patch

tests/wasmibctesting/utils.go#L523-L524

Added lines #L523 - L524 were not covered by tests
}

// CloseChannel close channel on both sides
func CloseChannel(coord *ibctesting.Coordinator, path *ibctesting.Path) {
err := path.EndpointA.ChanCloseInit()
Expand Down
52 changes: 51 additions & 1 deletion x/wasm/keeper/ibc2.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@
payload channeltypesv2.Payload,
relayer sdk.AccAddress,
) error {
contractAddr, err := ContractFromPortID2(payload.SourcePort)
if err != nil {
return errorsmod.Wrapf(err, "contract port id")

Check warning on line 78 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L76-L78

Added lines #L76 - L78 were not covered by tests
}
msg := wasmvmtypes.IBC2PacketTimeoutMsg{
Payload: newIBC2Payload(payload),
SourceClient: sourceClient,
DestinationClient: destinationClient,
PacketSequence: sequence,
Relayer: relayer.String(),

Check warning on line 85 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L80-L85

Added lines #L80 - L85 were not covered by tests
}
err = module.keeper.OnTimeoutIBC2Packet(ctx, contractAddr, msg)
if err != nil {
return errorsmod.Wrap(err, "on timeout")

Check warning on line 89 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L87-L89

Added lines #L87 - L89 were not covered by tests
}
return nil
}

Expand All @@ -96,7 +111,7 @@
contractAddr sdk.AccAddress,
msg wasmvmtypes.IBC2PacketReceiveMsg,
) channeltypesv2.RecvPacketResult {
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc-recv-packet")
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc2-recv-packet")

Check warning on line 114 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L114

Added line #L114 was not covered by tests
contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr)
if err != nil {
return channeltypesv2.RecvPacketResult{
Expand Down Expand Up @@ -165,6 +180,41 @@
}
}

// OnTimeoutIBC2Packet calls the contract to let it know the packet was never received
// on the destination chain within the timeout boundaries.
// The contract should handle this on the application level and undo the original operation
func (k Keeper) OnTimeoutIBC2Packet(
ctx sdk.Context,
contractAddr sdk.AccAddress,
msg wasmvmtypes.IBC2PacketTimeoutMsg,
) error {
defer telemetry.MeasureSince(time.Now(), "wasm", "contract", "ibc2-timeout-packet")

Check warning on line 191 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L190-L191

Added lines #L190 - L191 were not covered by tests

contractInfo, codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr)
if err != nil {
return err

Check warning on line 195 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L193-L195

Added lines #L193 - L195 were not covered by tests
}

env := types.NewEnv(ctx, contractAddr)
querier := k.newQueryHandler(ctx, contractAddr)

Check warning on line 199 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L198-L199

Added lines #L198 - L199 were not covered by tests

gasLeft := k.runtimeGasForContract(ctx)
res, gasUsed, execErr := k.wasmVM.IBC2PacketTimeout(codeInfo.CodeHash, env, msg, prefixStore, cosmwasmAPI, querier, ctx.GasMeter(), gasLeft, costJSONDeserialization)
k.consumeRuntimeGas(ctx, gasUsed)
if execErr != nil {
return errorsmod.Wrap(types.ErrExecuteFailed, execErr.Error())

Check warning on line 205 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L201-L205

Added lines #L201 - L205 were not covered by tests
}
if res == nil {

Check warning on line 207 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L207

Added line #L207 was not covered by tests
// If this gets executed, that's a bug in wasmvm
return errorsmod.Wrap(types.ErrVMError, "internal wasmvm error")

Check warning on line 209 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L209

Added line #L209 was not covered by tests
}
if res.Err != "" {
return types.MarkErrorDeterministic(errorsmod.Wrap(types.ErrExecuteFailed, res.Err))

Check warning on line 212 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L211-L212

Added lines #L211 - L212 were not covered by tests
}

return k.handleIBCBasicContractResponse(ctx, contractAddr, contractInfo.IBCPortID, res.Ok)

Check warning on line 215 in x/wasm/keeper/ibc2.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/ibc2.go#L215

Added line #L215 was not covered by tests
}

func newIBC2Payload(payload channeltypesv2.Payload) wasmvmtypes.IBC2Payload {
return wasmvmtypes.IBC2Payload{
SourcePort: payload.SourcePort,
Expand Down
8 changes: 8 additions & 0 deletions x/wasm/keeper/wasmtesting/mock_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
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)
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)
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)
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)
PinFn func(checksum wasmvm.Checksum) error
UnpinFn func(checksum wasmvm.Checksum) error
GetMetricsFn func() (*wasmvmtypes.Metrics, error)
Expand Down Expand Up @@ -113,6 +114,13 @@
return m.IBC2PacketReceiveFn(codeID, env, msg, store, goapi, querier, gasMeter, gasLimit, deserCost)
}

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) {
if m.IBC2PacketTimeoutFn == nil {
panic("not supposed to be called!")

Check warning on line 119 in x/wasm/keeper/wasmtesting/mock_engine.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/wasmtesting/mock_engine.go#L117-L119

Added lines #L117 - L119 were not covered by tests
}
return m.IBC2PacketTimeoutFn(codeID, env, msg, store, goapi, querier, gasMeter, gasLimit, deserCost)

Check warning on line 121 in x/wasm/keeper/wasmtesting/mock_engine.go

View check run for this annotation

Codecov / codecov/patch

x/wasm/keeper/wasmtesting/mock_engine.go#L121

Added line #L121 was not covered by tests
}

func (m *MockWasmEngine) StoreCode(codeID wasmvm.WasmCode, gasLimit uint64) (wasmvm.Checksum, uint64, error) {
if m.StoreCodeFn == nil {
panic("not supposed to be called!")
Expand Down
5 changes: 5 additions & 0 deletions x/wasm/types/exported_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,9 @@ type IBC2ContractKeeper interface {
contractAddr sdk.AccAddress,
msg wasmvmtypes.IBC2PacketReceiveMsg,
) channeltypesv2.RecvPacketResult
OnTimeoutIBC2Packet(
ctx sdk.Context,
contractAddr sdk.AccAddress,
msg wasmvmtypes.IBC2PacketTimeoutMsg,
) error
}
14 changes: 14 additions & 0 deletions x/wasm/types/wasmer_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,20 @@ type WasmEngine interface {
deserCost wasmvmtypes.UFraction,
) (*wasmvmtypes.IBCReceiveResult, uint64, error)

// IBC2PacketTimeout is available on IBCv2-enabled contracts and is called when an
// outgoing packet (previously sent by this contract) will probably never be executed.
IBC2PacketTimeout(
checksum wasmvm.Checksum,
env wasmvmtypes.Env,
packet wasmvmtypes.IBC2PacketTimeoutMsg,
store wasmvm.KVStore,
goapi wasmvm.GoAPI,
querier wasmvm.Querier,
gasMeter wasmvm.GasMeter,
gasLimit uint64,
deserCost wasmvmtypes.UFraction,
) (*wasmvmtypes.IBCBasicResult, uint64, error)

// Pin pins a code to an in-memory cache, such that is
// always loaded quickly when executed.
// Pin is idempotent.
Expand Down