Skip to content

Commit 132519b

Browse files
authored
Merge pull request #885 from CosmWasm/on-packet-recv-error-handling
Add a section on encoding IBCReceive errors
2 parents 7f73491 + e1e4207 commit 132519b

File tree

11 files changed

+399
-188
lines changed

11 files changed

+399
-188
lines changed

IBC.md

Lines changed: 188 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ Protobuf encoders for it as the protocol requires.
254254

255255
After a contract on chain A sends a packet, it is generally processed by the
256256
contract on chain B on the other side of the channel. This is done by executing
257-
the following callback on chain B:
257+
the following entry point on chain B:
258258

259259
```rust
260260
#[entry_point]
@@ -265,24 +265,35 @@ pub fn ibc_packet_receive(
265265
) -> StdResult<IbcReceiveResponse> { }
266266
```
267267

268-
Note the different return response here (`IbcReceiveResponse` rather than
269-
`IbcBasicResponse`)? This is because it has an extra field
270-
`acknowledgement: Binary`, which must be filled out. That is the response bytes
271-
that will be returned to the original contract, informing it of failure or
272-
success. (Note: this is vague as it will be refined in the next PR)
268+
This is a very special entry point as it has a unique workflow. (Please see the
269+
[Acknowledging Errors section](#Acknowledging-Errors) below to understand it
270+
fully).
273271

274-
Here is the
275-
[`IbcPacket` structure](https://github.com/CosmWasm/cosmwasm/blob/v0.14.0-beta4/packages/std/src/ibc.rs#L129-L146)
276-
that contains all information needed to process the receipt. You can generally
277-
ignore timeout (this is only called if it hasn't yet timed out) and sequence
278-
(which is used by the IBC framework to avoid duplicates). I generally use
279-
`dest.channel_id` like `info.sender` to authenticate the packet, and parse
280-
`data` into a `PacketMsg` structure, using the same encoding rules as we
281-
discussed in the last section.
272+
Also note the different return response here (`IbcReceiveResponse` rather than
273+
`IbcBasicResponse`). This is because it has an extra field
274+
`acknowledgement: Binary`, which must be filled out. All successful message must
275+
return an encoded `Acknowledgement` response in this field, that can be parsed
276+
by the sending chain.
282277

283-
After that you can process `PacketMsg` more or less like an `ExecuteMsg`,
284-
including calling into other contracts. The only major difference is that you
285-
must return Acknowledgement bytes in the protocol-specified format.
278+
The
279+
[`IbcPacket` structure](https://github.com/CosmWasm/cosmwasm/blob/v0.14.0-beta4/packages/std/src/ibc.rs#L129-L146)
280+
contains all information needed to process the receipt. This info has already
281+
been verified by the core IBC modules via light client and merkle proofs. It
282+
guarantees all metadata in the `IbcPacket` structure is valid, and the `data`
283+
field was written on the remote chain. Furthermore, it guarantees that the
284+
packet is processed at most once (zero times if it times out). Fields like
285+
`dest.channel_id` and `sequence` have a similar trust level to `MessageInfo`,
286+
which we use to authorize normal transactions. The `data` field should be
287+
treated like the `ExecuteMsg` data, which is only as valid as the entity that
288+
signed it.
289+
290+
You can generally ignore `timeout_*` (this entry point is only called if it
291+
hasn't yet timed out) and `sequence` (which is used by the IBC framework to
292+
avoid duplicates). I generally use `dest.channel_id` like `info.sender` to
293+
authenticate the packet, and parse `data` into a `PacketMsg` structure, using
294+
the same encoding rules as we discussed in the last section. After that you can
295+
process `PacketMsg` more or less like an `ExecuteMsg`, including calling into
296+
other contracts.
286297

287298
```rust
288299
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -305,10 +316,157 @@ pub struct IbcPacket {
305316
}
306317
```
307318

308-
TODO: explain how to handle/parse errors (As part of
309-
https://github.com/CosmWasm/cosmwasm/issues/762)
310-
311-
##### Standard Acknowledgement Format
319+
##### Acknowledging Errors
320+
321+
A major issue that is unique to `ibc_packet_receive` is that it is expected to
322+
often reject an incoming packet, yet it cannot abort the transaction. We
323+
actually expect all state changes from the contract (as well as dispatched
324+
messages) to be reverted when the packet is rejected, but the transaction to
325+
properly commit an acknowledgement with encoded error. In other words, this "IBC
326+
Handler" will error and revert, but the "IBC Router" must succeed and commit an
327+
acknowledgement message (that can be parsed by the sending chain as an error).
328+
329+
The atomicity issue was first
330+
[analyzed in the Cosmos SDK implementation](https://github.com/cosmos/ibc-go/issues/68)
331+
and refined into
332+
[changing semantics of the OnRecvPacket SDK method](https://github.com/cosmos/ibc-go/issues/91),
333+
which was
334+
[implemented in April 2021](https://github.com/cosmos/ibc-go/pull/107), likely
335+
to be released with Cosmos SDK 0.43 or 0.44. Since we want the best,
336+
future-proof interface for contracts, we will use an approach inspired by that
337+
work, and add an adapter in `wasmd` until we can upgrade to a Cosmos SDK version
338+
that implements this.
339+
340+
After quite some
341+
[discussion on how to encode the errors](https://github.com/CosmWasm/cosmwasm/issues/762),
342+
we struggled to map this idea to the CosmWasm model. However, we also discovered
343+
a deep similarity between these requirements and the
344+
[submessage semantics](./SEMANTICS.md#submessages). It just requires some
345+
careful coding on the contract developer's side to not throw errors. This
346+
produced 3 suggestions on how to handle errors and rollbacks _inside
347+
`ibc_packet_receive`_
348+
349+
1. If the message doesn't modify any state directly, you can simply put the
350+
logic in a closure, and capture errors, converting them into error
351+
acknowledgements. This would look something like the
352+
[main dispatch loop in `ibc-reflect`](https://github.com/CosmWasm/cosmwasm/blob/cd784cd1148ee395574f3e564f102d0d7b5adcc3/contracts/ibc-reflect/src/contract.rs#L217-L248):
353+
354+
```rust
355+
(|| {
356+
// which local channel did this packet come on
357+
let caller = packet.dest.channel_id;
358+
let msg: PacketMsg = from_slice(&packet.data)?;
359+
match msg {
360+
PacketMsg::Dispatch { msgs } => receive_dispatch(deps, caller, msgs),
361+
PacketMsg::WhoAmI {} => receive_who_am_i(deps, caller),
362+
PacketMsg::Balances {} => receive_balances(deps, caller),
363+
}
364+
})()
365+
.or_else(|e| {
366+
// we try to capture all app-level errors and convert them into
367+
// acknowledgement packets that contain an error code.
368+
let acknowledgement = encode_ibc_error(format!("invalid packet: {}", e));
369+
Ok(IbcReceiveResponse {
370+
acknowledgement,
371+
submessages: vec![],
372+
messages: vec![],
373+
attributes: vec![],
374+
})
375+
})
376+
```
377+
378+
2. If we modify state with an external call, we need to wrap it in a
379+
`submessage` and capture the error. This approach requires we use _exactly
380+
one_ submessage. If we have multiple, we may commit #1 and rollback #2 (see
381+
example 3 for that case). The main point is moving `messages` to
382+
`submessages` and reformating the error in `reply`. Note that if you set the
383+
`Response.data` field in `reply` it will override the acknowledgement
384+
returned from the parent call. (See
385+
[bottom of reply section](./SEMANTICS.md#handling-the-reply)). You can see a
386+
similar example in how
387+
[`ibc-reflect` handles `receive_dispatch`](https://github.com/CosmWasm/cosmwasm/blob/eebb9395ccf315320e3f2fcc526ee76788f89174/contracts/ibc-reflect/src/contract.rs#L307-L336).
388+
Note how we use a unique reply ID for this and use that to catch any
389+
execution failure and return an error acknowledgement instead:
390+
391+
```rust
392+
fn receive_dispatch(
393+
deps: DepsMut,
394+
caller: String,
395+
msgs: Vec<CosmosMsg>,
396+
) -> StdResult<IbcReceiveResponse> {
397+
// what is the reflect contract here
398+
let reflect_addr = accounts(deps.storage).load(caller.as_bytes())?;
399+
400+
// let them know we're fine
401+
let acknowledgement = to_binary(&AcknowledgementMsg::<DispatchResponse>::Ok(()))?;
402+
// create the message to re-dispatch to the reflect contract
403+
let reflect_msg = ReflectExecuteMsg::ReflectMsg { msgs };
404+
let wasm_msg = wasm_execute(reflect_addr, &reflect_msg, vec![])?;
405+
406+
// we wrap it in a submessage to properly report errors
407+
let sub_msg = SubMsg {
408+
id: RECEIVE_DISPATCH_ID,
409+
msg: wasm_msg.into(),
410+
gas_limit: None,
411+
reply_on: ReplyOn::Error,
412+
};
413+
414+
Ok(IbcReceiveResponse {
415+
acknowledgement,
416+
submessages: vec![sub_msg],
417+
messages: vec![],
418+
attributes: vec![attr("action", "receive_dispatch")],
419+
})
420+
}
421+
422+
#[entry_point]
423+
pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> StdResult<Response> {
424+
match (reply.id, reply.result) {
425+
(RECEIVE_DISPATCH_ID, ContractResult::Err(err)) => Ok(Response {
426+
data: Some(encode_ibc_error(err)),
427+
..Response::default()
428+
}),
429+
(INIT_CALLBACK_ID, ContractResult::Ok(response)) => handle_init_callback(deps, response),
430+
_ => Err(StdError::generic_err("invalid reply id or result")),
431+
}
432+
}
433+
```
434+
435+
3. For a more complex case, where we are modifying local state and possibly
436+
sending multiple messages, we need to do a self-call via submessages. What I
437+
mean is that we create a new `ExecuteMsg` variant, which returns an error if
438+
called by anyone but the contract itself
439+
(`if info.sender != env.contract.address { return Err() }`). When receiving
440+
the IBC packet, we can create a submessage with `ExecuteMsg::DoReceivePacket`
441+
and any args we need to pass down.
442+
443+
`DoReceivePacket` should return a proper acknowledgement payload on success.
444+
And return an error on failure, just like a normal `execute` call. However,
445+
here we capture both success and error cases in the `reply` handler (use
446+
`ReplyOn::Always`). For success, we return this data verbatim to be set as
447+
the packet acknowledgement, and for errors, we encode them as we did above.
448+
There is not any example code using this (yet), but it is just recombining
449+
pieces we already have. For clarity, the `reply` statement should look
450+
something like:
451+
452+
```rust
453+
#[entry_point]
454+
pub fn reply(_deps: DepsMut, _env: Env, reply: Reply) -> StdResult<Response> {
455+
if reply.id != DO_IBC_RECEIVE_ID {
456+
return Err(StdError::generic_err("invalid reply id"));
457+
}
458+
let data = match reply.result {
459+
ContractResult::Ok(response) => response.data,
460+
ContractResult::Err(err) => Some(encode_ibc_error(err)),
461+
};
462+
Ok(Response {
463+
data,
464+
..Response::default()
465+
})
466+
}
467+
```
468+
469+
##### Standard Acknowledgement Envelope
312470

313471
Although the ICS spec leave the actual acknowledgement as opaque bytes, it does
314472
provide a recommendation for the format you can use, allowing contracts to
@@ -330,12 +488,18 @@ message Acknowledgement {
330488

331489
Although it suggests this is a Protobuf object, the ICS spec doesn't define
332490
whether to encode it as JSON or Protobuf. In the ICS20 implementation, this is
333-
JSON encoded when returned from a contract. Given that, we will consider this
334-
structure, JSON-encoded, to be the "standard" acknowledgement format.
491+
JSON encoded when returned from a contract. In ICS27, the authors are discussing
492+
using a Protobuf-encoded form of this structure.
493+
494+
Note that it leaves the actual success response as app-specific bytes where you
495+
can place anything, but does provide a standard way for an observer to check
496+
success-or-error. If you are designing a new protocol, I encourage you to use
497+
this struct in either of the encodings as the acknowledgement envelope.
335498

336499
You can find a
337500
[CosmWasm-compatible definition of this format](https://github.com/CosmWasm/cosmwasm-plus/blob/v0.6.0-beta1/contracts/cw20-ics20/src/ibc.rs#L52-L72)
338-
as part of the `cw20-ics20` contract.
501+
as part of the `cw20-ics20` contract, along with JSON-encoding. Protobuf
502+
encoding version can be produced upon request.
339503

340504
#### Receiving an Acknowledgement
341505

contracts/ibc-reflect/examples/schema.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use std::fs::create_dir_all;
44
use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for};
55

66
use ibc_reflect::msg::{
7-
AcknowledgementMsg, BalancesResponse, DispatchResponse, ExecuteMsg, InstantiateMsg, PacketMsg,
8-
QueryMsg, WhoAmIResponse,
7+
AcknowledgementMsg, BalancesResponse, DispatchResponse, InstantiateMsg, PacketMsg, QueryMsg,
8+
WhoAmIResponse,
99
};
1010

1111
fn main() {
@@ -14,7 +14,6 @@ fn main() {
1414
create_dir_all(&out_dir).unwrap();
1515
remove_schemas(&out_dir).unwrap();
1616

17-
export_schema(&schema_for!(ExecuteMsg), &out_dir);
1817
export_schema(&schema_for!(InstantiateMsg), &out_dir);
1918
export_schema(&schema_for!(QueryMsg), &out_dir);
2019
export_schema(&schema_for!(PacketMsg), &out_dir);

contracts/ibc-reflect/schema/execute_msg.json

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)