From 12c00ec85c1238a0b9075a14593485c60f0da729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?colin=20axn=C3=A9r?= <25233464+colin-axner@users.noreply.github.com> Date: Wed, 22 Mar 2023 17:30:10 +0100 Subject: [PATCH] imp: address ADR 8 review suggestions (#3319) --------- Co-authored-by: Damian Nolan --- docs/architecture/README.md | 1 + .../adr-008-app-caller-cbs.md | 191 +++++++++++------- 2 files changed, 119 insertions(+), 73 deletions(-) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 213cbc138b2..cbea1a892f6 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -31,6 +31,7 @@ To suggest an ADR, please make use of the [ADR template](./adr-template.md) prov | [002](./adr-002-go-module-versioning.md) | Go module versioning | Accepted | | [003](./adr-003-ics27-acknowledgement.md) | ICS27 acknowledgement format | Accepted | | [004](./adr-004-ics29-lock-fee-module.md) | ICS29 module locking upon escrow out of balance | Accepted | +| [008](./adr-008-app-caller-cbs/adr-008-app-caller-cbs.md) | Callback to IBC ACtors | Accepted | | [015](./adr-015-ibc-packet-receiver.md) | IBC Packet Routing | Accepted | | [025](./adr-025-ibc-passive-channels.md) | IBC passive channels | Deprecated | | [026](./adr-026-ibc-client-recovery-mechanisms.md) | IBC client recovery mechansisms | Accepted | diff --git a/docs/architecture/adr-008-app-caller-cbs/adr-008-app-caller-cbs.md b/docs/architecture/adr-008-app-caller-cbs/adr-008-app-caller-cbs.md index 486ae8abcb7..b67275ccf49 100644 --- a/docs/architecture/adr-008-app-caller-cbs/adr-008-app-caller-cbs.md +++ b/docs/architecture/adr-008-app-caller-cbs/adr-008-app-caller-cbs.md @@ -1,11 +1,13 @@ # ADR 008: Callback to IBC Actors ## Changelog + * 2022-08-10: Initial Draft +* 2023-03-22: Merged ## Status -Proposed +Accepted, packet callback interface implemented ## Context @@ -51,7 +53,7 @@ type IBCActor interface { type PacketActor interface { // OnRecvPacket will be called on the IBCActor after the IBC Application // handles the RecvPacket callback if the packet has an IBC Actor as a receiver. - OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, relayer string) error + OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error // OnAcknowledgementPacket will be called on the IBC Actor // after the IBC Application handles its own OnAcknowledgementPacket callback @@ -59,7 +61,7 @@ type PacketActor interface { ctx sdk.Context, packet channeltypes.Packet, ack exported.Acknowledgement, - relayer string + relayer sdk.AccAddress, ) error // OnTimeoutPacket will be called on the IBC Actor @@ -67,24 +69,31 @@ type PacketActor interface { OnTimeoutPacket( ctx sdk.Context, packet channeltypes.Packet, - relayer string + relayer sdk.AccAddress, ) error } ``` -The CallbackPacketData interface will get created to add `GetSrcCallbackAddress` and `GetDestCallbackAddress` methods. These may return an address -or they may return the empty string. The address may reference an IBCActor or it may be a regular user address. If the address is not an IBCActor, the actor callback must continue processing (no-op). Any IBC application or middleware that uses these methods must handle these cases. In most cases, the `GetSrcCallbackAddress` will be the sender address and the `GetDestCallbackAddress` will be the receiver address. However, these are named generically so that implementors may choose a different contract address for the callback if they choose. +The `CallbackPacketData` interface will get created to add `GetSourceCallbackAddress` and `GetDestCallbackAddress` methods. These may return an address +or they may return the empty string. The address may reference an PacketActor or it may be a regular user address. If the address is not a PacketActor, the actor callback must continue processing (no-op). Any IBC application or middleware that uses these methods must handle these cases. In most cases, the `GetSourceCallbackAddress` will be the sender address and the `GetDestCallbackAddress` will be the receiver address. However, these are named generically so that implementors may choose a different contract address for the callback if they choose. + +The interface also defines a `UserDefinedGasLimit` method. Any middleware targeting this interface for callback handling should cap the gas that a callback is allowed to take (especially on AcknowledgePacket and TimeoutPacket) so that a custom callback does not prevent the packet lifecycle from completing. However, since this is a global cap it is likely to be very large. Thus, users may specify a smaller limit to cap the amount of fees a relayer must pay in order to complete the packet lifecycle on the user's behalf. -The interface also defines a `UserDefinedGasLimit` method. Any middleware targetting this interface for callback handling should cap the gas that a callback is allowed to take (especially on AcknowledgePacket and TimeoutPacket) so that a custom callback does not prevent the packet lifecycle from completing. However, since this is a global cap it is likely to be very large. Thus, users may specify a smaller limit to cap the amount of fees a relayer must pay in order to complete the packet lifecycle on the user's behalf. +IBC applications which provide the base packet data type must implement the `CallbackPacketData` interface to allow `PacketActor` callbacks. ```go -// Implemented by any packet data type that wants to support -// PacketActor callbacks +// Implemented by any packet data type that wants to support PacketActor callbacks +// PacketActor's will be unable to act on any packet data type that does not implement +// this interface. type CallbackPacketData interface { - // may return the empty string - GetSrcCallbackAddress() string - - // may return the empty string + // GetSourceCallbackAddress should return the callback address of a packet data on the source chain. + // This may or may not be the sender of the packet. If no source callback address exists for the packet, + // an empty string may be returned. + GetSourceCallbackAddress() string + + // GetDestCallbackAddress should return the callback address of a packet data on the destination chain. + // This may or may not be the receiver of the packet. If no dest callback address exists for the packet, + // an empty string may be returned. GetDestCallbackAddress() string // UserDefinedGasLimit allows the sender of the packet to define inside the packet data @@ -103,7 +112,8 @@ IBC Apps or middleware can then call the IBCActor callbacks like so in their own ### Handshake Callbacks -The handshake init callbacks (`OnChanOpenInit` and `OnChanCloseInit`) will need to include an additional field so that the initiating actor can be tracked and called upon during handshake completion. +The `OnChanOpenInit` handshake callback will need to include an additional field so that the initiating actor can be tracked and called upon during handshake completion. +The actor provided in the `OnChanOpenInit` callback will be the signer of the `MsgChanOpenInit` message. ```go func OnChanOpenInit( @@ -141,42 +151,26 @@ func OnChanOpenAck( ibcActor, _ := acc.(IBCActor) ibcActor.OnChanOpen(ctx, portID, channelID, version) } - // cleanup state - k.deleteActor(ctx, portID, channelID) + + // the same actor will be used for channel closure } -func OnChanOpenConfirm( +func OnChanCloseInit( ctx sdk.Context, portID, - channelID string, + channelID, ) error { // run any necesssary logic first - // retrieve final version actor := k.getActor(ctx, portID, channelID) if actor != "" { ibcActor, _ := acc.(IBCActor) - ibcActor.OnChanOpen(ctx, portID, channelID, version) + ibcActor.OnChanClose(ctx, portID, channelID) } // cleanup state k.deleteActor(ctx, portID, channelID) } -func OnChanCloseInit( - ctx sdk.Context, - portID, - channelID, - actor string, -) error { - acc := k.getAccount(ctx, actor) - ibcActor, ok := acc.(IBCActor) - if ok { - k.setActor(ctx, portID, channelID, actor) - } - - // continued logic -} - func OnChanCloseConfirm( ctx sdk.Context, portID, @@ -194,14 +188,16 @@ func OnChanCloseConfirm( } ``` +NOTE: The handshake calls `OnChanOpenTry` and `OnChanOpenConfirm` are explicitly left out as it is still to be determined how the actor of the `OnChanOpenTry` step should be provided. Initially only the initiating side of the channel handshake may support setting a channel actor, future improvements should allow both sides of the channel handshake to set channel actors. + ### PacketCallbacks No packet callback API will need to change. ```go // Call the IBCActor recvPacket callback after processing the packet -// if the recvPacket callback exists and returns an error -// then return an error ack to revert all packet data processing +// if the recvPacket callback exists. If the callback returns an error +// then return an error ack to revert all packet data processing. func OnRecvPacket( ctx sdk.Context, packet channeltypes.Packet, @@ -210,10 +206,16 @@ func OnRecvPacket( // run any necesssary logic first // IBCActor logic will postprocess + // postprocessing should only if the underlying application + // returns a successful ack + // unmarshal packet data into expected interface var cbPacketData callbackPacketData unmarshalInterface(packet.GetData(), cbPacketData) + if cbPacketData == nil { + // the packet data does not implement the CallbackPacketData interface + // continue processing (no-op) return } @@ -229,6 +231,21 @@ func OnRecvPacket( // deduct consumed gas from original context ctx = ctx.WithGasLimit(ctx.GasMeter().RemainingGas() - cbCtx.GasMeter().GasConsumed()) if err != nil { + // NOTE: by returning an error acknowledgement, it is assumed that the + // base IBC application on the counterparty callback stack will be able + // to properly unmarshal the error acknowledgement. It should not expect + // some custom error acknowledgement. If it does, failed acknowledgements + // will be unsuccessfully processed which can be catastrophic in processing + // refund logic. + // + // If this issue is a serious concern, an ADR 8 implementation can construct its own + // acknowledgement type which wraps the underlying application acknowledgement. This + // would require deployment on both sides of the packet flow, in addition to version + // negotiation to enable the custom acknowledgement type usage. + // + // Future improvmenets should allow for each IBC application in a stack of + // callbacks to provide their own acknowledgement without disrupting the unmarshaling + // of an application above or below it in the stack. return AcknowledgementError(err) } } @@ -245,14 +262,17 @@ func (im IBCModule) OnAcknowledgementPacket( ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, - relayer string, + relayer sdk.AccAddress, ) error { // application-specific onAcknowledgmentPacket logic // unmarshal packet data into expected interface var cbPacketData callbackPacketData unmarshalInterface(packet.GetData(), cbPacketData) + if cbPacketData == nil { + // the packet data does not implement the CallbackPacketData interface + // continue processing (no-op) return } @@ -261,32 +281,42 @@ func (im IBCModule) OnAcknowledgementPacket( unmarshal(acknowledgement, ack) // send acknowledgement to original actor - acc := k.getAccount(ctx, cbPacketData.GetSrcCallbackAddress()) + acc := k.getAccount(ctx, cbPacketData.GetSourceCallbackAddress()) ibcActor, ok := acc.(IBCActor) if ok { gasLimit := getGasLimit(ctx, cbPacketData) - // create cached context with gas limit - cacheCtx, writeFn := ctx.CacheContext() - cacheCtx = cacheCtx.WithGasLimit(gasLimit) + + handleCallback := func() error { + // create cached context with gas limit + cacheCtx, writeFn := ctx.CacheContext() + cacheCtx = cacheCtx.WithGasLimit(gasLimit) - defer func() { - if e := recover(); e != nil { - log("ran out of gas in callback. reverting callback state") - } else { - // only write callback state if we did not panic during execution - writeFn() + defer func() { + if e := recover(); e != nil { + log("ran out of gas in callback. reverting callback state") + } else if err == nil { + // only write callback state if we did not panic during execution + // and the error returned is nil + writeFn() + } } - } - err := ibcActor.OnAcknowledgementPacket(cacheCtx, packet, ack, relayer) + err := ibcActor.OnAcknowledgementPacket(cacheCtx, packet, ack, relayer) - // deduct consumed gas from original context - ctx = ctx.WithGasLimit(ctx.GasMeter().RemainingGas() - cbCtx.GasMeter().GasConsumed()) + // deduct consumed gas from original context + ctx = ctx.WithGasLimit(ctx.GasMeter().RemainingGas() - cbCtx.GasMeter().GasConsumed()) + + return err + } - setAckCallbackError(ctx, packet, err) - emitAckCallbackErrorEvents(err) + if err := handleCallback(); err != nil { + setAckCallbackError(ctx, packet, err) // optional + emitAckCallbackErrorEvents(err) + } } + + return nil } // Call the IBCActor timeoutPacket callback after processing the packet @@ -298,44 +328,56 @@ func (im IBCModule) OnAcknowledgementPacket( func (im IBCModule) OnTimeoutPacket( ctx sdk.Context, packet channeltypes.Packet, - relayer string, + relayer sdk.AccAddress, ) error { // application-specific onTimeoutPacket logic // unmarshal packet data into expected interface var cbPacketData callbackPacketData unmarshalInterface(packet.GetData(), cbPacketData) + if cbPacketData == nil { + // the packet data does not implement the CallbackPacketData interface + // continue processing (no-op) return } // call timeout callback on original actor - acc := k.getAccount(ctx, cbPacketData.GetSrcCallbackAddress()) + acc := k.getAccount(ctx, cbPacketData.GetSourceCallbackAddress()) ibcActor, ok := acc.(IBCActor) if ok { gasLimit := getGasLimit(ctx, cbPacketData) - // create cached context with gas limit - cacheCtx, writeFn := ctx.CacheContext() - cacheCtx = cacheCtx.WithGasLimit(gasLimit) + handleCallback := func() error { + // create cached context with gas limit + cacheCtx, writeFn := ctx.CacheContext() + cacheCtx = cacheCtx.WithGasLimit(gasLimit) - defer func() { - if e := recover(); e != nil { - log("ran out of gas in callback. reverting callback state") - } else { - // only write callback state if we did not panic during execution - writeFn() + defer func() { + if e := recover(); e != nil { + log("ran out of gas in callback. reverting callback state") + } else if err == nil { + // only write callback state if we did not panic during execution + // and the error returned is nil + writeFn() + } } - } - err := ibcActor.OnTimeoutPacket(ctx, packet, relayer) + err := ibcActor.OnTimeoutPacket(ctx, packet, relayer) - // deduct consumed gas from original context - ctx = ctx.WithGasLimit(ctx.GasMeter().RemainingGas() - cbCtx.GasMeter().GasConsumed()) + // deduct consumed gas from original context + ctx = ctx.WithGasLimit(ctx.GasMeter().RemainingGas() - cbCtx.GasMeter().GasConsumed()) - setTimeoutCallbackError(ctx, packet, err) - emitTimeoutCallbackErrorEvents(err) + return err + } + + if err := handleCallback(); err != nil { + setTimeoutCallbackError(ctx, packet, err) // optional + emitTimeoutCallbackErrorEvents(err) + } } + + return nil } func getGasLimit(ctx sdk.Context, cbPacketData CallbackPacketData) uint64 { @@ -367,10 +409,13 @@ Chains are expected to specify a `chainDefinedActorCallbackLimit` to ensure that ### Negative - Callbacks may now have unbounded gas consumption since the actor may execute arbitrary logic. Chains implementing this feature should take care to place limitations on how much gas an actor callback can consume. -- Application packets that want to support ADR-8 must additionally have their packet data implement the `CallbackPacketData` interface and register their implementation on the chain codec ### Neutral +- Application packets that want to support ADR-8 must additionally have their packet data implement the `CallbackPacketData` interface and register their implementation on the chain codec + ## References -- https://github.com/cosmos/ibc-go/issues/1660 +- [Original issue](https://github.com/cosmos/ibc-go/issues/1660) +- [CallbackPacketData interface implementation](https://github.com/cosmos/ibc-go/pull/3287) +- [ICS 20, ICS 27 implementations of the CallbackPacketData interface](https://github.com/cosmos/ibc-go/pull/3287)