Skip to content

Commit

Permalink
added packet timeouts to wasm hooks (#3862)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolaslara authored and czarcas7ic committed Jan 4, 2023
1 parent 168ccda commit de76db0
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 15 deletions.
Binary file added tests/ibc-hooks/bytecode/counter.wasm
Binary file not shown.
34 changes: 34 additions & 0 deletions tests/ibc-hooks/ibc_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"testing"
"time"

wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"

Expand Down Expand Up @@ -460,6 +461,39 @@ func (suite *HooksTestSuite) TestAcks() {

}

func (suite *HooksTestSuite) TestTimeouts() {
suite.chainA.StoreContractCode(&suite.Suite, "./bytecode/counter.wasm")
addr := suite.chainA.InstantiateContract(&suite.Suite, `{"count": 0}`, 1)

// Generate swap instructions for the contract
callbackMemo := fmt.Sprintf(`{"ibc_callback":"%s"}`, addr)
// Send IBC transfer with the memo with crosschain-swap instructions
transferMsg := NewMsgTransfer(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1000)), suite.chainA.SenderAccount.GetAddress().String(), addr.String(), callbackMemo)
transferMsg.TimeoutTimestamp = uint64(suite.coordinator.CurrentTime.Add(time.Minute).UnixNano())
sendResult, err := suite.chainA.SendMsgsNoCheck(transferMsg)
suite.Require().NoError(err)

packet, err := ibctesting.ParsePacketFromEvents(sendResult.GetEvents())
suite.Require().NoError(err)

// Move chainB forward one block
suite.chainB.NextBlock()
// One month later
suite.coordinator.IncrementTimeBy(time.Hour)
err = suite.path.EndpointA.UpdateClient()
suite.Require().NoError(err)

err = suite.path.EndpointA.TimeoutPacket(packet)
suite.Require().NoError(err)

// The test contract will increment the counter for itself by 10 when a packet times out
state := suite.chainA.QueryContract(
&suite.Suite, addr,
[]byte(fmt.Sprintf(`{"get_count": {"addr": "%s"}}`, addr)))
suite.Require().Equal(`{"count":10}`, state)

}

func (suite *HooksTestSuite) TestSendWithoutMemo() {
// Sending a packet without memo to ensure that the ibc_callback middleware doesn't interfere with a regular send
transferMsg := NewMsgTransfer(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1000)), suite.chainA.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), "")
Expand Down
65 changes: 50 additions & 15 deletions x/ibc-hooks/testutils/contracts/counter/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,60 @@ pub mod execute {
}

pub fn reset(deps: DepsMut, info: MessageInfo, count: i32) -> Result<Response, ContractError> {
COUNTERS.update(
deps.storage,
info.sender.clone(),
|state| -> Result<_, ContractError> {
match state {
None => Err(ContractError::Unauthorized {}),
Some(state) if state.owner != info.sender.clone() => {
Err(ContractError::Unauthorized {})
}
_ => Ok(Counter {
count,
total_funds: vec![],
owner: info.sender.clone(),
}),
}
utils::update_counter(deps, info.sender, &|_counter| count, &|_counter| vec![])?;
Ok(Response::new().add_attribute("action", "reset"))
}
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result<Response, ContractError> {
match msg {
SudoMsg::ReceiveAck {
channel: _,
sequence: _,
ack: _,
success,
} => sudo::receive_ack(deps, env.contract.address, success),
SudoMsg::IBCTimeout {
channel: _,
sequence: _,
} => sudo::ibc_timeout(deps, env.contract.address),
}
}

pub mod sudo {
use cosmwasm_std::Addr;

use super::*;

pub fn receive_ack(
deps: DepsMut,
contract: Addr,
_success: bool,
) -> Result<Response, ContractError> {
utils::update_counter(
deps,
contract,
&|counter| match counter {
None => 1,
Some(counter) => counter.count + 1,
},
)?;
Ok(Response::new().add_attribute("action", "reset"))
}

pub(crate) fn ibc_timeout(deps: DepsMut, contract: Addr) -> Result<Response, ContractError> {
utils::update_counter(
deps,
contract,
&|counter| match counter {
None => 10,
Some(counter) => counter.count + 10,
},
&|_counter| vec![],
)?;
Ok(Response::new().add_attribute("action", "timeout"))
}
}

pub fn naive_add_coins(lhs: &Vec<Coin>, rhs: &Vec<Coin>) -> Vec<Coin> {
Expand Down
12 changes: 12 additions & 0 deletions x/ibc-hooks/testutils/contracts/counter/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ pub struct GetCountResponse {
pub struct GetTotalFundsResponse {
pub total_funds: Vec<Coin>,
}

#[cw_serde]
pub enum SudoMsg {
ReceiveAck {
channel: String,
sequence: u64,
ack: String,
success: bool,
},
#[serde(rename = "ibc_timeout")]
IBCTimeout { channel: String, sequence: u64 },
}
155 changes: 155 additions & 0 deletions x/ibc-hooks/wasm_hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
sdk "github.com/cosmos/cosmos-sdk/types"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported"
Expand Down Expand Up @@ -204,3 +205,157 @@ func ValidateAndParseMemo(memo string, receiver string) (isWasmRouted bool, cont

return isWasmRouted, contractAddr, msgBytes, nil
}

func (h WasmHooks) SendPacketOverride(i ICS4Middleware, ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI) error {
concretePacket, ok := packet.(channeltypes.Packet)
if !ok {
return i.channel.SendPacket(ctx, chanCap, packet) // continue
}

isIcs20, data := isIcs20Packet(concretePacket)
if !isIcs20 {
return i.channel.SendPacket(ctx, chanCap, packet) // continue
}

isCallbackRouted, metadata := jsonStringHasKey(data.GetMemo(), types.IBCCallbackKey)
if !isCallbackRouted {
return i.channel.SendPacket(ctx, chanCap, packet) // continue
}

// We remove the callback metadata from the memo as it has already been processed.

// If the only available key in the memo is the callback, we should remove the memo
// from the data completely so the packet is sent without it.
// This way receiver chains that are on old versions of IBC will be able to process the packet

callbackRaw := metadata[types.IBCCallbackKey] // This will be used later.
delete(metadata, types.IBCCallbackKey)
bzMetadata, err := json.Marshal(metadata)
if err != nil {
return sdkerrors.Wrap(err, "Send packet with callback error")
}
stringMetadata := string(bzMetadata)
if stringMetadata == "{}" {
data.Memo = ""
} else {
data.Memo = stringMetadata
}
dataBytes, err := json.Marshal(data)
if err != nil {
return sdkerrors.Wrap(err, "Send packet with callback error")
}

packetWithoutCallbackMemo := channeltypes.Packet{
Sequence: concretePacket.Sequence,
SourcePort: concretePacket.SourcePort,
SourceChannel: concretePacket.SourceChannel,
DestinationPort: concretePacket.DestinationPort,
DestinationChannel: concretePacket.DestinationChannel,
Data: dataBytes,
TimeoutTimestamp: concretePacket.TimeoutTimestamp,
TimeoutHeight: concretePacket.TimeoutHeight,
}

err = i.channel.SendPacket(ctx, chanCap, packetWithoutCallbackMemo)
if err != nil {
return err
}

// Make sure the callback contract is a string and a valid bech32 addr. If it isn't, ignore this packet
contract, ok := callbackRaw.(string)
if !ok {
return nil
}
_, err = sdk.AccAddressFromBech32(contract)
if err != nil {
return nil
}

h.ibcHooksKeeper.StorePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence(), contract)
return nil
}

func (h WasmHooks) OnAcknowledgementPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error {
err := im.App.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer)
if err != nil {
return err
}

if !h.ProperlyConfigured() {
// Not configured. Return from the underlying implementation
return nil
}

contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
if contract == "" {
// No callback configured
return nil
}

contractAddr, err := sdk.AccAddressFromBech32(contract)
if err != nil {
return sdkerrors.Wrap(err, "Ack callback error") // The callback configured is not a bech32. Error out
}

success := "false"
if !osmoutils.IsAckError(acknowledgement) {
success = "true"
}

// Notify the sender that the ack has been received
ackAsJson, err := json.Marshal(acknowledgement)
if err != nil {
// If the ack is not a json object, error
return err
}

sudoMsg := []byte(fmt.Sprintf(
`{"receive_ack": {"channel": "%s", "sequence": %d, "ack": %s, "success": %s}}`,
packet.SourceChannel, packet.Sequence, ackAsJson, success))
_, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg)
if err != nil {
// error processing the callback
// ToDo: Open Question: Should we also delete the callback here?
return sdkerrors.Wrap(err, "Ack callback error")
}
h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
return nil
}

func (h WasmHooks) OnTimeoutPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error {
err := im.App.OnTimeoutPacket(ctx, packet, relayer)
if err != nil {
return err
}

if !h.ProperlyConfigured() {
// Not configured. Return from the underlying implementation
return nil
}

contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
if contract == "" {
// No callback configured
return nil
}

contractAddr, err := sdk.AccAddressFromBech32(contract)
if err != nil {
return sdkerrors.Wrap(err, "Timeout callback error") // The callback configured is not a bech32. Error out
}

sudoMsg := []byte(fmt.Sprintf(
`{"ibc_timeout": {"channel": "%s", "sequence": %d}}`,
packet.SourceChannel, packet.Sequence))
_, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg)
if err != nil {
// error processing the callback. This could be because the contract doesn't implement the message type to
// process the callback. Retrying this will not help, so we delete the callback from storage.
// Since the packet has timed out, we don't expect any other responses that may trigger the callback.
h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
return sdkerrors.Wrap(err, "Timeout callback error")
}
//
h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
return nil
}

0 comments on commit de76db0

Please sign in to comment.