diff --git a/internal/api/bindings.h b/internal/api/bindings.h index 6a1bdd86e..6e778a96f 100644 --- a/internal/api/bindings.h +++ b/internal/api/bindings.h @@ -597,6 +597,30 @@ struct UnmanagedVector ibc_packet_timeout(struct cache_t *cache, struct GasReport *gas_report, struct UnmanagedVector *error_msg); +struct UnmanagedVector ibc_source_callback(struct cache_t *cache, + struct ByteSliceView checksum, + struct ByteSliceView env, + struct ByteSliceView msg, + struct Db db, + struct GoApi api, + struct GoQuerier querier, + uint64_t gas_limit, + bool print_debug, + struct GasReport *gas_report, + struct UnmanagedVector *error_msg); + +struct UnmanagedVector ibc_destination_callback(struct cache_t *cache, + struct ByteSliceView checksum, + struct ByteSliceView env, + struct ByteSliceView msg, + struct Db db, + struct GoApi api, + struct GoQuerier querier, + uint64_t gas_limit, + bool print_debug, + struct GasReport *gas_report, + struct UnmanagedVector *error_msg); + struct UnmanagedVector new_unmanaged_vector(bool nil, const uint8_t *ptr, uintptr_t length); void destroy_unmanaged_vector(struct UnmanagedVector v); diff --git a/internal/api/lib.go b/internal/api/lib.go index c73c55976..56924437e 100644 --- a/internal/api/lib.go +++ b/internal/api/lib.go @@ -700,6 +700,90 @@ func IBCPacketTimeout( return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil } +func IBCSourceCallback( + cache Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + cs := makeView(checksum) + defer runtime.KeepAlive(checksum) + e := makeView(env) + defer runtime.KeepAlive(env) + msgBytes := makeView(msg) + defer runtime.KeepAlive(msg) + var pinner runtime.Pinner + pinner.Pin(gasMeter) + checkAndPinAPI(api, pinner) + checkAndPinQuerier(querier, pinner) + defer pinner.Unpin() + + callID := startCall() + defer endCall(callID) + + dbState := buildDBState(store, callID) + db := buildDB(&dbState, gasMeter) + a := buildAPI(api) + q := buildQuerier(querier) + var gasReport C.GasReport + errmsg := uninitializedUnmanagedVector() + + res, err := C.ibc_source_callback(cache.ptr, cs, e, msgBytes, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) + if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { + // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. + return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) + } + return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil +} + +func IBCDestinationCallback( + cache Cache, + checksum []byte, + env []byte, + msg []byte, + gasMeter *types.GasMeter, + store types.KVStore, + api *types.GoAPI, + querier *Querier, + gasLimit uint64, + printDebug bool, +) ([]byte, types.GasReport, error) { + cs := makeView(checksum) + defer runtime.KeepAlive(checksum) + e := makeView(env) + defer runtime.KeepAlive(env) + msgBytes := makeView(msg) + defer runtime.KeepAlive(msg) + var pinner runtime.Pinner + pinner.Pin(gasMeter) + checkAndPinAPI(api, pinner) + checkAndPinQuerier(querier, pinner) + defer pinner.Unpin() + + callID := startCall() + defer endCall(callID) + + dbState := buildDBState(store, callID) + db := buildDB(&dbState, gasMeter) + a := buildAPI(api) + q := buildQuerier(querier) + var gasReport C.GasReport + errmsg := uninitializedUnmanagedVector() + + res, err := C.ibc_destination_callback(cache.ptr, cs, e, msgBytes, db, a, q, cu64(gasLimit), cbool(printDebug), &gasReport, &errmsg) + if err != nil && err.(syscall.Errno) != C.ErrnoValue_Success { + // Depending on the nature of the error, `gasUsed` will either have a meaningful value, or just 0. + return nil, convertGasReport(gasReport), errorWithMessage(err, errmsg) + } + return copyAndDestroyUnmanagedVector(res), convertGasReport(gasReport), nil +} + func convertGasReport(report C.GasReport) types.GasReport { return types.GasReport{ Limit: uint64(report.limit), diff --git a/lib_libwasmvm.go b/lib_libwasmvm.go index 7a0c53e58..e4b28bd4a 100644 --- a/lib_libwasmvm.go +++ b/lib_libwasmvm.go @@ -532,6 +532,75 @@ func (vm *VM) IBCPacketTimeout( return &result, gasReport.UsedInternally, nil } +// IBCSourceCallback is available on IBC-enabled contracts with the corresponding entrypoint +// and should be called when the response (ack or timeout) for an outgoing callbacks-enabled packet +// (previously sent by this contract) is received. +func (vm *VM) IBCSourceCallback( + checksum Checksum, + env types.Env, + msg types.IBCSourceCallbackMsg, + store KVStore, + goapi GoAPI, + querier Querier, + gasMeter GasMeter, + gasLimit uint64, + deserCost types.UFraction, +) (*types.IBCBasicResult, uint64, error) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + msgBin, err := json.Marshal(msg) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.IBCSourceCallback(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.IBCBasicResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + +// IBCDestinationCallback is available on IBC-enabled contracts with the corresponding entrypoint +// and should be called when an incoming callbacks-enabled IBC packet is received. +func (vm *VM) IBCDestinationCallback( + checksum Checksum, + env types.Env, + msg types.IBCDestinationCallbackMsg, + store KVStore, + goapi GoAPI, + querier Querier, + gasMeter GasMeter, + gasLimit uint64, + deserCost types.UFraction, +) (*types.IBCBasicResult, uint64, error) { + envBin, err := json.Marshal(env) + if err != nil { + return nil, 0, err + } + msgBin, err := json.Marshal(msg) + if err != nil { + return nil, 0, err + } + data, gasReport, err := api.IBCDestinationCallback(vm.cache, checksum, envBin, msgBin, &gasMeter, store, &goapi, &querier, gasLimit, vm.printDebug) + if err != nil { + return nil, gasReport.UsedInternally, err + } + + var result types.IBCBasicResult + err = DeserializeResponse(gasLimit, deserCost, &gasReport, data, &result) + if err != nil { + return nil, gasReport.UsedInternally, err + } + return &result, gasReport.UsedInternally, nil +} + func compileCost(code WasmCode) uint64 { // CostPerByte is how much CosmWasm gas is charged *per byte* for compiling WASM code. // Benchmarks and numbers (in SDK Gas) were discussed in: diff --git a/libwasmvm/Cargo.lock b/libwasmvm/Cargo.lock index e30f7ffd0..a3525a2a1 100644 --- a/libwasmvm/Cargo.lock +++ b/libwasmvm/Cargo.lock @@ -1146,9 +1146,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -1786,7 +1786,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -2067,7 +2067,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -2080,7 +2080,7 @@ version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -2326,7 +2326,7 @@ dependencies = [ "bytesize", "derive_builder", "hex", - "indexmap 2.2.5", + "indexmap 2.2.6", "schemars", "semver", "serde", @@ -2417,7 +2417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" dependencies = [ "bitflags 2.4.2", - "indexmap 2.2.5", + "indexmap 2.2.6", "semver", ] diff --git a/libwasmvm/bindings.h b/libwasmvm/bindings.h index 6a1bdd86e..6e778a96f 100644 --- a/libwasmvm/bindings.h +++ b/libwasmvm/bindings.h @@ -597,6 +597,30 @@ struct UnmanagedVector ibc_packet_timeout(struct cache_t *cache, struct GasReport *gas_report, struct UnmanagedVector *error_msg); +struct UnmanagedVector ibc_source_callback(struct cache_t *cache, + struct ByteSliceView checksum, + struct ByteSliceView env, + struct ByteSliceView msg, + struct Db db, + struct GoApi api, + struct GoQuerier querier, + uint64_t gas_limit, + bool print_debug, + struct GasReport *gas_report, + struct UnmanagedVector *error_msg); + +struct UnmanagedVector ibc_destination_callback(struct cache_t *cache, + struct ByteSliceView checksum, + struct ByteSliceView env, + struct ByteSliceView msg, + struct Db db, + struct GoApi api, + struct GoQuerier querier, + uint64_t gas_limit, + bool print_debug, + struct GasReport *gas_report, + struct UnmanagedVector *error_msg); + struct UnmanagedVector new_unmanaged_vector(bool nil, const uint8_t *ptr, uintptr_t length); void destroy_unmanaged_vector(struct UnmanagedVector v); diff --git a/libwasmvm/src/calls.rs b/libwasmvm/src/calls.rs index 44d57b36d..38d8fbf99 100644 --- a/libwasmvm/src/calls.rs +++ b/libwasmvm/src/calls.rs @@ -8,9 +8,10 @@ use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use cosmwasm_std::Checksum; use cosmwasm_vm::{ call_execute_raw, call_ibc_channel_close_raw, call_ibc_channel_connect_raw, - call_ibc_channel_open_raw, call_ibc_packet_ack_raw, call_ibc_packet_receive_raw, - call_ibc_packet_timeout_raw, call_instantiate_raw, call_migrate_raw, call_query_raw, - call_reply_raw, call_sudo_raw, Backend, Cache, Instance, InstanceOptions, VmResult, + call_ibc_channel_open_raw, call_ibc_destination_callback_raw, call_ibc_packet_ack_raw, + call_ibc_packet_receive_raw, call_ibc_packet_timeout_raw, call_ibc_source_callback_raw, + call_instantiate_raw, call_migrate_raw, call_query_raw, call_reply_raw, call_sudo_raw, Backend, + Cache, Instance, InstanceOptions, VmResult, }; use crate::api::GoApi; @@ -395,6 +396,66 @@ pub extern "C" fn ibc_packet_timeout( ) } +#[no_mangle] +pub extern "C" fn ibc_source_callback( + cache: *mut cache_t, + checksum: ByteSliceView, + env: ByteSliceView, + msg: ByteSliceView, + db: Db, + api: GoApi, + querier: GoQuerier, + gas_limit: u64, + print_debug: bool, + gas_report: Option<&mut GasReport>, + error_msg: Option<&mut UnmanagedVector>, +) -> UnmanagedVector { + call_2_args( + call_ibc_source_callback_raw, + cache, + checksum, + env, + msg, + db, + api, + querier, + gas_limit, + print_debug, + gas_report, + error_msg, + ) +} + +#[no_mangle] +pub extern "C" fn ibc_destination_callback( + cache: *mut cache_t, + checksum: ByteSliceView, + env: ByteSliceView, + msg: ByteSliceView, + db: Db, + api: GoApi, + querier: GoQuerier, + gas_limit: u64, + print_debug: bool, + gas_report: Option<&mut GasReport>, + error_msg: Option<&mut UnmanagedVector>, +) -> UnmanagedVector { + call_2_args( + call_ibc_destination_callback_raw, + cache, + checksum, + env, + msg, + db, + api, + querier, + gas_limit, + print_debug, + gas_report, + error_msg, + ) +} + type VmFn2Args = fn( instance: &mut Instance, arg1: &[u8], diff --git a/types/ibc.go b/types/ibc.go index 1e2b4b223..4fc69acae 100644 --- a/types/ibc.go +++ b/types/ibc.go @@ -148,6 +148,47 @@ type IBCPacketTimeoutMsg struct { Relayer string `json:"relayer"` } +// The type of IBC source callback that is being called. +// +// IBC source callbacks are needed for cases where your contract triggers the sending of an IBC packet through some other message (i.e. not through [`IbcMsg::SendPacket`]) and needs to know whether or not the packet was successfully received on the other chain. A prominent example is the [`IbcMsg::Transfer`] message. Without callbacks, you cannot know whether the transfer was successful or not. +// +// Note that there are some prerequisites that need to be fulfilled to receive source callbacks: - The contract must implement the `ibc_source_callback` entrypoint. - The IBC application in the source chain must have support for the callbacks middleware. - You have to add serialized [`IbcCallbackRequest`] to a specific field of the message. For `IbcMsg::Transfer`, this is the `memo` field and it needs to be json-encoded. - The receiver of the callback must also be the sender of the message. +type IBCSourceCallbackMsg struct { + Acknowledgement *IBCAckCallbackMsg `json:"acknowledgement,omitempty"` + Timeout *IBCTimeoutCallbackMsg `json:"timeout,omitempty"` +} + +type IBCAckCallbackMsg struct { + Acknowledgement IBCAcknowledgement `json:"acknowledgement"` + OriginalPacket IBCPacket `json:"original_packet"` + Relayer string `json:"relayer"` +} + +type IBCTimeoutCallbackMsg struct { + Packet IBCPacket `json:"packet"` + Relayer string `json:"relayer"` +} + +// The message type of the IBC destination callback. +// +// The IBC destination callback is needed for cases where someone triggers the sending of an +// IBC packet through some other message (i.e. not through [`IbcMsg::SendPacket`]) and +// your contract needs to know that it received this. +// The callback is called after the packet was successfully acknowledged on the destination chain. +// A prominent example is the [`IbcMsg::Transfer`] message. Without callbacks, you cannot know +// that someone sent you IBC coins. +// +// Note that there are some prerequisites that need to be fulfilled to receive source callbacks: +// - The contract must implement the `ibc_destination_callback` entrypoint. +// - The module that receives the packet must be wrapped by an `IBCMiddleware` +// (i.e. the destination chain needs to support callbacks for the message you are being sent). +// - You have to add json-encoded [`IbcCallbackData`] to a specific field of the message. +// For `IbcMsg::Transfer`, this is the `memo` field. +type IBCDestinationCallbackMsg struct { + Ack IBCAcknowledgement `json:"ack"` + Packet IBCPacket `json:"packet"` +} + // TODO: test what the sdk Order.String() represents and how to parse back // Proto files: https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/ibc/core/channel/v1/channel.proto#L69-L80 // Auto-gen code: https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/x/ibc/core/04-channel/types/channel.pb.go#L70-L101