Skip to content

Commit 7d23e0f

Browse files
AdityaSripalcrodriguezvegacolin-axner
authored
ADR 008: IBC Actor Callbacks (#1976)
* context and decision * complete adr * Apply suggestions from code review Co-authored-by: Carlos Rodriguez <carlos@interchain.io> * change from caller to generalized actor * Apply suggestions from code review Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com> * create folder and scaffolded middleware * add error handling and generify packetdata interface * complete renaming * add user defined gas limit and clarify pseudocode * Clarify CallbackPacketData interface imp: Add ADR 008: IBC Actor Callbacks --------- Co-authored-by: Carlos Rodriguez <carlos@interchain.io> Co-authored-by: colin axnér <25233464+colin-axner@users.noreply.github.com>
1 parent 5a67efc commit 7d23e0f

File tree

2 files changed

+599
-0
lines changed

2 files changed

+599
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
# ADR 008: Callback to IBC Actors
2+
3+
## Changelog
4+
* 2022-08-10: Initial Draft
5+
6+
## Status
7+
8+
Proposed
9+
10+
## Context
11+
12+
IBC was designed with callbacks between core IBC and IBC applications. IBC apps would send a packet to core IBC. When the result of the packet lifecycle eventually resolved into either an acknowledgement or a timeout, core IBC called a callback on the IBC application so that the IBC application could take action on the basis of the result (e.g. unescrow tokens for ICS-20).
13+
14+
This setup worked well for off-chain users interacting with IBC applications.
15+
16+
We are now seeing the desire for secondary applications (e.g. smart contracts, modules) to call into IBC apps as part of their state machine logic and then do some actions on the basis of the packet result. Or to receive a packet from IBC and do some logic upon receipt.
17+
18+
Example Usecases:
19+
- Send an ICS-20 packet, and if it is successful, then send an ICA-packet to swap tokens on LP and return funds to sender
20+
- Execute some logic upon receipt of token transfer to a smart contract address
21+
22+
This requires a second layer of callbacks. The IBC application already gets the result of the packet from core IBC, but currently there is no standardized way to pass this information on to an actor module/smart contract.
23+
24+
## Definitions
25+
26+
- Actor: an actor is an on-chain module (this may be a hardcoded module in the chain binary or a smart contract) that wishes to execute custom logic whenever IBC receives a packet flow that it has either sent or received. It **must** be addressable by a string value.
27+
28+
## Decision
29+
30+
Create a standardized callback interface that actors can implement. IBC applications (or middleware that wraps IBC applications) can now call this callback to route the result of the packet/channel handshake from core IBC to the IBC application to the original actor on the sending chain. IBC applications can route the packet receipt to the destination actor on the receiving chain.
31+
32+
IBC actors may implement the following interface:
33+
34+
```go
35+
type IBCActor interface {
36+
// OnChannelOpen will be called on the IBCActor when the channel opens
37+
// this will happen either on ChanOpenAck or ChanOpenConfirm
38+
OnChannelOpen(ctx sdk.Context, portID, channelID, version string)
39+
40+
// OnChannelClose will be called on the IBCActor if the channel closes
41+
// this will be called on either ChanCloseInit or ChanCloseConfirm and if the channel handshake fails on our end
42+
// NOTE: currently the channel does not automatically close if the counterparty fails the handhshake so actors must be prepared for an OpenInit to never return a callback for the time being
43+
OnChannelClose(ctx sdk.Context, portID, channelID string)
44+
45+
// IBCActor must also implement PacketActor interface
46+
PacketActor
47+
}
48+
49+
// PacketActor is split out into its own separate interface since implementors may choose
50+
// to only support callbacks for packet methods rather than supporting the full IBCActor interface
51+
type PacketActor interface {
52+
// OnRecvPacket will be called on the IBCActor after the IBC Application
53+
// handles the RecvPacket callback if the packet has an IBC Actor as a receiver.
54+
OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, relayer string) error
55+
56+
// OnAcknowledgementPacket will be called on the IBC Actor
57+
// after the IBC Application handles its own OnAcknowledgementPacket callback
58+
OnAcknowledgmentPacket(
59+
ctx sdk.Context,
60+
packet channeltypes.Packet,
61+
ack exported.Acknowledgement,
62+
relayer string
63+
) error
64+
65+
// OnTimeoutPacket will be called on the IBC Actor
66+
// after the IBC Application handles its own OnTimeoutPacket callback
67+
OnTimeoutPacket(
68+
ctx sdk.Context,
69+
packet channeltypes.Packet,
70+
relayer string
71+
) error
72+
}
73+
```
74+
75+
The CallbackPacketData interface will get created to add `GetSrcCallbackAddress` and `GetDestCallbackAddress` methods. These may return an address
76+
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.
77+
78+
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.
79+
80+
```go
81+
// Implemented by any packet data type that wants to support
82+
// PacketActor callbacks
83+
type CallbackPacketData interface {
84+
// may return the empty string
85+
GetSrcCallbackAddress() string
86+
87+
// may return the empty string
88+
GetDestCallbackAddress() string
89+
90+
// UserDefinedGasLimit allows the sender of the packet to define inside the packet data
91+
// a gas limit for how much the ADR-8 callbacks can consume. If defined, this will be passed
92+
// in as the gas limit so that the callback is guaranteed to complete within a specific limit.
93+
// On recvPacket, a gas-overflow will just fail the transaction allowing it to timeout on the sender side.
94+
// On ackPacket and timeoutPacket, a gas-overflow will reject state changes made during callback but still
95+
// commit the transaction. This ensures the packet lifecycle can always complete.
96+
// If the packet data returns 0, the remaining gas limit will be passed in (modulo any chain-defined limit)
97+
// Otherwise, we will set the gas limit passed into the callback to the `min(ctx.GasLimit, UserDefinedGasLimit())`
98+
UserDefinedGasLimit() uint64
99+
}
100+
```
101+
102+
IBC Apps or middleware can then call the IBCActor callbacks like so in their own callbacks:
103+
104+
### Handshake Callbacks
105+
106+
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.
107+
108+
```go
109+
func OnChanOpenInit(
110+
ctx sdk.Context,
111+
order channeltypes.Order,
112+
connectionHops []string,
113+
portID string,
114+
channelID string,
115+
channelCap *capabilitytypes.Capability,
116+
counterparty channeltypes.Counterparty,
117+
version string,
118+
actor string,
119+
) (string, error) {
120+
acc := k.getAccount(ctx, actor)
121+
ibcActor, ok := acc.(IBCActor)
122+
if ok {
123+
k.setActor(ctx, portID, channelID, actor)
124+
}
125+
126+
// continued logic
127+
}
128+
129+
func OnChanOpenAck(
130+
ctx sdk.Context,
131+
portID,
132+
channelID string,
133+
counterpartyChannelID string,
134+
counterpartyVersion string,
135+
) error {
136+
// run any necessary logic first
137+
// negotiate final version
138+
139+
actor := k.getActor(ctx, portID, channelID)
140+
if actor != "" {
141+
ibcActor, _ := acc.(IBCActor)
142+
ibcActor.OnChanOpen(ctx, portID, channelID, version)
143+
}
144+
// cleanup state
145+
k.deleteActor(ctx, portID, channelID)
146+
}
147+
148+
func OnChanOpenConfirm(
149+
ctx sdk.Context,
150+
portID,
151+
channelID string,
152+
) error {
153+
// run any necesssary logic first
154+
// retrieve final version
155+
156+
actor := k.getActor(ctx, portID, channelID)
157+
if actor != "" {
158+
ibcActor, _ := acc.(IBCActor)
159+
ibcActor.OnChanOpen(ctx, portID, channelID, version)
160+
}
161+
// cleanup state
162+
k.deleteActor(ctx, portID, channelID)
163+
}
164+
165+
func OnChanCloseInit(
166+
ctx sdk.Context,
167+
portID,
168+
channelID,
169+
actor string,
170+
) error {
171+
acc := k.getAccount(ctx, actor)
172+
ibcActor, ok := acc.(IBCActor)
173+
if ok {
174+
k.setActor(ctx, portID, channelID, actor)
175+
}
176+
177+
// continued logic
178+
}
179+
180+
func OnChanCloseConfirm(
181+
ctx sdk.Context,
182+
portID,
183+
channelID string,
184+
) error {
185+
// run any necesssary logic first
186+
187+
actor := k.getActor(ctx, portID, channelID)
188+
if actor != "" {
189+
ibcActor, _ := acc.(IBCActor)
190+
ibcActor.OnChanClose(ctx, portID, channelID)
191+
}
192+
// cleanup state
193+
k.deleteActor(ctx, portID, channelID)
194+
}
195+
```
196+
197+
### PacketCallbacks
198+
199+
No packet callback API will need to change.
200+
201+
```go
202+
// Call the IBCActor recvPacket callback after processing the packet
203+
// if the recvPacket callback exists and returns an error
204+
// then return an error ack to revert all packet data processing
205+
func OnRecvPacket(
206+
ctx sdk.Context,
207+
packet channeltypes.Packet,
208+
relayer sdk.AccAddress,
209+
) (ack exported.Acknowledgement) {
210+
// run any necesssary logic first
211+
// IBCActor logic will postprocess
212+
213+
// unmarshal packet data into expected interface
214+
var cbPacketData callbackPacketData
215+
unmarshalInterface(packet.GetData(), cbPacketData)
216+
if cbPacketData == nil {
217+
return
218+
}
219+
220+
acc := k.getAccount(ctx, cbPacketData.GetDstCallbackAddress())
221+
ibcActor, ok := acc.(IBCActor)
222+
if ok {
223+
// set gas limit for callback
224+
gasLimit := getGasLimit(ctx, cbPacketData)
225+
cbCtx = ctx.WithGasLimit(gasLimit)
226+
227+
err := ibcActor.OnRecvPacket(cbCtx, packet, relayer)
228+
229+
// deduct consumed gas from original context
230+
ctx = ctx.WithGasLimit(ctx.GasMeter().RemainingGas() - cbCtx.GasMeter().GasConsumed())
231+
if err != nil {
232+
return AcknowledgementError(err)
233+
}
234+
}
235+
return
236+
}
237+
238+
// Call the IBCActor acknowledgementPacket callback after processing the packet
239+
// if the ackPacket callback exists and returns an error
240+
// DO NOT return the error upstream. The acknowledgement must complete for the packet
241+
// lifecycle to end, so the custom callback cannot block completion.
242+
// Instead we emit error events and set the error in state
243+
// so that users and on-chain logic can handle this appropriately
244+
func (im IBCModule) OnAcknowledgementPacket(
245+
ctx sdk.Context,
246+
packet channeltypes.Packet,
247+
acknowledgement []byte,
248+
relayer string,
249+
) error {
250+
// application-specific onAcknowledgmentPacket logic
251+
252+
// unmarshal packet data into expected interface
253+
var cbPacketData callbackPacketData
254+
unmarshalInterface(packet.GetData(), cbPacketData)
255+
if cbPacketData == nil {
256+
return
257+
}
258+
259+
// unmarshal ack bytes into the acknowledgment interface
260+
var ack exported.Acknowledgement
261+
unmarshal(acknowledgement, ack)
262+
263+
// send acknowledgement to original actor
264+
acc := k.getAccount(ctx, cbPacketData.GetSrcCallbackAddress())
265+
ibcActor, ok := acc.(IBCActor)
266+
if ok {
267+
gasLimit := getGasLimit(ctx, cbPacketData)
268+
269+
// create cached context with gas limit
270+
cacheCtx, writeFn := ctx.CacheContext()
271+
cacheCtx = cacheCtx.WithGasLimit(gasLimit)
272+
273+
defer func() {
274+
if e := recover(); e != nil {
275+
log("ran out of gas in callback. reverting callback state")
276+
} else {
277+
// only write callback state if we did not panic during execution
278+
writeFn()
279+
}
280+
}
281+
282+
err := ibcActor.OnAcknowledgementPacket(cacheCtx, packet, ack, relayer)
283+
284+
// deduct consumed gas from original context
285+
ctx = ctx.WithGasLimit(ctx.GasMeter().RemainingGas() - cbCtx.GasMeter().GasConsumed())
286+
287+
setAckCallbackError(ctx, packet, err)
288+
emitAckCallbackErrorEvents(err)
289+
}
290+
}
291+
292+
// Call the IBCActor timeoutPacket callback after processing the packet
293+
// if the timeoutPacket callback exists and returns an error
294+
// DO NOT return the error upstream. The timeout must complete for the packet
295+
// lifecycle to end, so the custom callback cannot block completion.
296+
// Instead we emit error events and set the error in state
297+
// so that users and on-chain logic can handle this appropriately
298+
func (im IBCModule) OnTimeoutPacket(
299+
ctx sdk.Context,
300+
packet channeltypes.Packet,
301+
relayer string,
302+
) error {
303+
// application-specific onTimeoutPacket logic
304+
305+
// unmarshal packet data into expected interface
306+
var cbPacketData callbackPacketData
307+
unmarshalInterface(packet.GetData(), cbPacketData)
308+
if cbPacketData == nil {
309+
return
310+
}
311+
312+
// call timeout callback on original actor
313+
acc := k.getAccount(ctx, cbPacketData.GetSrcCallbackAddress())
314+
ibcActor, ok := acc.(IBCActor)
315+
if ok {
316+
gasLimit := getGasLimit(ctx, cbPacketData)
317+
318+
// create cached context with gas limit
319+
cacheCtx, writeFn := ctx.CacheContext()
320+
cacheCtx = cacheCtx.WithGasLimit(gasLimit)
321+
322+
defer func() {
323+
if e := recover(); e != nil {
324+
log("ran out of gas in callback. reverting callback state")
325+
} else {
326+
// only write callback state if we did not panic during execution
327+
writeFn()
328+
}
329+
}
330+
331+
err := ibcActor.OnTimeoutPacket(ctx, packet, relayer)
332+
333+
// deduct consumed gas from original context
334+
ctx = ctx.WithGasLimit(ctx.GasMeter().RemainingGas() - cbCtx.GasMeter().GasConsumed())
335+
336+
setTimeoutCallbackError(ctx, packet, err)
337+
emitTimeoutCallbackErrorEvents(err)
338+
}
339+
}
340+
341+
func getGasLimit(ctx sdk.Context, cbPacketData CallbackPacketData) uint64 {
342+
// getGasLimit returns the gas limit to pass into the actor callback
343+
// this will be the minimum of the remaining gas limit in the tx
344+
// and the config defined gas limit. The config limit is itself
345+
// the minimum of a user defined gas limit and the chain-defined gas limit
346+
// for actor callbacks
347+
var configLimit uint64
348+
if cbPacketData == 0 {
349+
configLimit = chainDefinedActorCallbackLimit
350+
} else {
351+
configLimit = min(chainDefinedActorCallbackLimit, cbPacketData.UserDefinedGasLimit())
352+
}
353+
return min(ctx.GasMeter().GasRemaining(), configLimit)
354+
}
355+
```
356+
357+
Chains are expected to specify a `chainDefinedActorCallbackLimit` to ensure that callbacks do not consume an arbitrary amount of gas. Thus, it should always be possible for a relayer to complete the packet lifecycle even if the actor callbacks cannot run successfully.
358+
359+
## Consequences
360+
361+
### Positive
362+
363+
- IBC Actors can now programatically execute logic that involves sending a packet and then performing some additional logic once the packet lifecycle is complete
364+
- Middleware implementing ADR-8 can be generally used for any application
365+
- Leverages the same callback architecture used between core IBC and IBC applications
366+
367+
### Negative
368+
369+
- 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.
370+
- 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
371+
372+
### Neutral
373+
374+
## References
375+
376+
- https://github.com/cosmos/ibc-go/issues/1660

0 commit comments

Comments
 (0)