classic-core icon indicating copy to clipboard operation
classic-core copied to clipboard

TIP #49 Upgrade CosmWasm dependency from 0.16 to 1.0

Open yun-yeo opened this issue 2 years ago • 0 comments

TIP #: 49

Author: TFL Network: v0.6.x Date:

Summary

Upgrade CosmWasm from 0.16 to 1.0.

Motivation

CosmWasm 0.16 does not currently support full IBC-Wasm functionality. Upgrading to CosmWasm 1.0 allows Terra to adopt IBC-Wasm integration, enabling contract execution between IBC enabled chains.

Tech Spec

Modules

  • Wasm

Overview

This upgrade will allow IBC-Wasm functionality to be enabled, establishing channels and relaying packets between Wasm contracts over chains. Contracts that support IBC messages will be detected during instantiation and reviewed for compatibility. This upgrade also supports the [ICS20 protocol](https://github.com/CosmWasm/cosmwasm/blob/main/IBC.md#sending-tokens-via-ics20), which is used to move fungible tokens between Cosmos blockchains.

More information can be found in the [IBC Specification](https://github.com/CosmWasm/wasmd/blob/master/x/wasm/IBC.md).

Method

  1. Bump wasmvm version to 1.0.0.
  2. Check IBC entry points when instantiating or migrating the contract to give a unique port ID.
  3. Implement IBC message handler to relay the packet to the proper IBC channel.

Code

The new field IBCPortID is added to ContractInfo which requires proto changes. This field addition does not break backward compatibility. See [this article about updating Proto definitions](https://developers.google.com/protocol-buffers/docs/overview#updating-defs) for more information.

// ContractInfo stores a WASM contract instance
type ContractInfo struct {
	...

	// IBCPortID is the assigned IBC port ID. This ID can only be used in a contract.
	IBCPortID string `protobuf:"bytes,6,opt,name=ibc_port_id,json=ibcPortId,proto3" json:"ibc_port_id,omitempty" yaml:"ibc_port_id"`
}
// ContractInfo stores a WASM contract instance:
message ContractInfo {
	...

  // IBCPortID is the assigned IBC port ID. This ID can only be used in a contract.
  string ibc_port_id = 6 [(gogoproto.moretags) = "yaml:\"ibc_port_id\"", (gogoproto.customname) = "IBCPortID" ];
}

Each contract can have a unique port ID including previously instantiated contracts:

const portIDPrefix = "wasm."

// PortIDForContract build port ID from a contract address
func PortIDForContract(addr sdk.AccAddress) string {
	return portIDPrefix + addr.String()
}

When instantiating or migrating a contract, check that the contract has IBC entry points and set the IBCPortID:

// InstantiateContract creates an instance of a WASM contract.
func (k Keeper) InstantiateContract(
	...

	// Check for IBC flag
	report, err := k.wasmVM.AnalyzeCode(codeInfo.CodeHash)
	if err != nil {
		return nil, nil, sdkerrors.Wrap(types.ErrInstantiateFailed, err.Error())
	}

	// register IBC port
	if report.HasIBCEntryPoints {
		ibcPort, err := k.ensureIbcPort(ctx, contractAddress)
		if err != nil {
			return nil, nil, err
		}

		contractInfo.IBCPortID = ibcPort
	}

	...
}
// MigrateContract allows you to upgrade a contract to a new code with data migration.
func (k Keeper) MigrateContract(
	...

	// check for IBC flag
	switch report, err := k.wasmVM.AnalyzeCode(newCodeInfo.CodeHash); {
	case err != nil:
		return nil, sdkerrors.Wrap(types.ErrMigrationFailed, err.Error())
	case !report.HasIBCEntryPoints && contractInfo.IBCPortID != "":
		// prevent update to non ibc contract
		return nil, sdkerrors.Wrap(types.ErrMigrationFailed, "requires ibc callbacks")
	case report.HasIBCEntryPoints && contractInfo.IBCPortID == "":
		// add ibc port
		ibcPort, err := k.ensureIbcPort(ctx, contractAddress)
		if err != nil {
			return nil, err
		}

		contractInfo.IBCPortID = ibcPort
	}

	...
}

On each wasm execution, the response messages can now be IBC messages. A handler is added for the IBC messages:

// dispatchMessage does not emit events, preventing duplicate emissions
func (k Keeper) dispatchMessage(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events sdk.Events, data []byte, err error) {

	// only the contract itself can send a packet with its ibc port ID
	if msg.IBC != nil && msg.IBC.SendPacket != nil {
		ibcEvents, err := k.messenger.HandleIBCSendPacket(ctx, contractIBCPortID, msg)
		if err != nil {
			return nil, nil, err
		}

		return ibcEvents, nil, nil
	}

	...
}
// HandleIBCSendPacket implement Messeger
func (messenger Messenger) HandleIBCSendPacket(ctx sdk.Context, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (sdk.Events, error) {
	if contractIBCPortID == "" {
		return nil, types.ErrUnsupportedForContract
	}

	sendPacket := msg.IBC.SendPacket
	contractIBCChannelID := sendPacket.ChannelID
	if contractIBCChannelID == "" {
		return nil, types.ErrEmpty
	}

	sequence, found := channelKeeper.GetNextSequenceSend(ctx, contractIBCPortID, contractIBCChannelID)
	if !found {
		return nil, channeltypes.ErrSequenceSendNotFound
	}

	channelInfo, ok := channelKeeper.GetChannel(ctx, contractIBCPortID, contractIBCChannelID)
	if !ok {
		return nil, channeltypes.ErrInvalidChannel
	}

	capabilityPath := host.ChannelCapabilityPath(contractIBCPortID, contractIBCChannelID)
	channelCap, ok := capabilityKeeper.GetCapability(ctx, capabilityPath)
	if !ok {
		return nil, channeltypes.ErrChannelCapabilityNotFound
	}

	packet := channeltypes.NewPacket(
		msg.IBC.SendPacket.Data,
		sequence,
		contractIBCPortID,
		contractIBCChannelID,
		channelInfo.Counterparty.PortId,
		channelInfo.Counterparty.ChannelId,
		types.ConvertWasmIBCTimeoutHeightToCosmosHeight(msg.IBC.SendPacket.Timeout.Block),
		msg.IBC.SendPacket.Timeout.Timestamp,
	)

	err := channelKeeper.SendPacket(ctx, channelCap, packet)
	if err != nil {
		return nil, err
	}

	return ctx.EventManager().Events(), nil
}

Considerations

Adding an IBCPortID field will not break backward compatibility. See [this article about updating Proto definitions](https://developers.google.com/protocol-buffers/docs/overview#updating-defs) for more information.

Timeline

Test cases

  1. TestIBC_instantiate ensures the ability to instantiate the IBC enabled contract with portID generation:
func TestIBC_instantiate(t *testing.T) {
	ibcWasmCode, err := ioutil.ReadFile("./testdata/ibc_reflect.wasm")
	require.NoError(t, err)

	reflectWasmCode, err := ioutil.ReadFile("./testdata/reflect.wasm")
	require.NoError(t, err)

	ibcCodeID, err := keeper.StoreCode(ctx, creator, ibcWasmCode)
	require.NoError(t, err)

	reflectCodeID, err := keeper.StoreCode(ctx, creator, reflectWasmCode)
	require.NoError(t, err)

	ibcInitMsg := IBCReflectInitMsg{ReflectCodeID: reflectCodeID}
	ibcInitMsgBz, err := json.Marshal(ibcInitMsg)
	require.NoError(t, err)

	addr, _, err := keeper.InstantiateContract(ctx, ibcCodeID, creator, sdk.AccAddress{}, ibcInitMsgBz, nil)
	require.NoError(t, err)

	cInfo, err := keeper.GetContractInfo(ctx, addr)
	require.NoError(t, err)
	assert.Equal(t, ibcCodeID, cInfo.CodeID)
	assert.Equal(t, cInfo.IBCPortID, types.PortIDForContract(addr))
}
  1. TestIBC_migrateNormalToIBC ensures the ability to migrate a contract to the IBC enabled contract:
func TestIBC_migrateNormalToIBC(t *testing.T) {
	ibcWasmCode, err := ioutil.ReadFile("./testdata/ibc_reflect.wasm")
	require.NoError(t, err)

	reflectWasmCode, err := ioutil.ReadFile("./testdata/reflect.wasm")
	require.NoError(t, err)

	ibcCodeID, err := keeper.StoreCode(ctx, creator, ibcWasmCode)
	require.NoError(t, err)

	reflectCodeID, err := keeper.StoreCode(ctx, creator, reflectWasmCode)
	require.NoError(t, err)

	addr, _, err := keeper.InstantiateContract(ctx, reflectCodeID, creator, creator, []byte("{}"), nil)
	require.NoError(t, err)

	cInfo, err := keeper.GetContractInfo(ctx, addr)
	require.NoError(t, err)
	assert.Equal(t, ibcCodeID, cInfo.CodeID)
	assert.Equal(t, cInfo.IBCPortID, "")

	_, err = keeper.MigrateContract(ctx, addr, creator, ibcCodeID, []byte("{}"))
	require.NoError(t, err)

	cInfo, err = keeper.GetContractInfo(ctx, addr)
	require.NoError(t, err)
	assert.Equal(t, ibcCodeID, cInfo.CodeID)
	assert.Equal(t, cInfo.IBCPortID, types.PortIDForContract(addr))
}
  1. TestIBC_migrateIBCToIBC ensures the ability to migrate an IBC enabled contract to the other IBC enabled contract:
func TestIBC_migrateIBCToIBC(t *testing.T) {
	ibcWasmCode, err := ioutil.ReadFile("./testdata/ibc_reflect.wasm")
	require.NoError(t, err)

	reflectWasmCode, err := ioutil.ReadFile("./testdata/reflect.wasm")
	require.NoError(t, err)

	ibcCodeID, err := keeper.StoreCode(ctx, creator, ibcWasmCode)
	require.NoError(t, err)

	reflectCodeID, err := keeper.StoreCode(ctx, creator, reflectWasmCode)
	require.NoError(t, err)

	ibcInitMsg := IBCReflectInitMsg{ReflectCodeID: reflectCodeID}
	ibcInitMsgBz, err := json.Marshal(ibcInitMsg)
	require.NoError(t, err)

	addr, _, err := keeper.InstantiateContract(ctx, ibcCodeID, creator, sdk.AccAddress{}, ibcInitMsgBz, nil)
	require.NoError(t, err)

	cInfo, err := keeper.GetContractInfo(ctx, addr)
	require.NoError(t, err)
	assert.Equal(t, ibcCodeID, cInfo.CodeID)
	assert.Equal(t, cInfo.IBCPortID, types.PortIDForContract(addr))

	_, err = keeper.MigrateContract(ctx, addr, creator, ibcCodeID, []byte("{}"))
	require.NoError(t, err)

	cInfo, err = keeper.GetContractInfo(ctx, addr)
	require.NoError(t, err)
	assert.Equal(t, ibcCodeID, cInfo.CodeID)
	assert.Equal(t, cInfo.IBCPortID, types.PortIDForContract(addr))
}
  1. TestIBC_migrateIBCToNormal ensures the inability to migrate an IBC enabled contract to a normal contract:
func TestIBC_migrateIBCToNormal(t *testing.T) {
	ibcWasmCode, err := ioutil.ReadFile("./testdata/ibc_reflect.wasm")
	require.NoError(t, err)

	reflectWasmCode, err := ioutil.ReadFile("./testdata/reflect.wasm")
	require.NoError(t, err)

	ibcCodeID, err := keeper.StoreCode(ctx, creator, ibcWasmCode)
	require.NoError(t, err)

	reflectCodeID, err := keeper.StoreCode(ctx, creator, reflectWasmCode)
	require.NoError(t, err)

	ibcInitMsg := IBCReflectInitMsg{ReflectCodeID: reflectCodeID}
	ibcInitMsgBz, err := json.Marshal(ibcInitMsg)
	require.NoError(t, err)

	addr, _, err := keeper.InstantiateContract(ctx, ibcCodeID, creator, sdk.AccAddress{}, ibcInitMsgBz, nil)
	require.NoError(t, err)

	cInfo, err := keeper.GetContractInfo(ctx, addr)
	require.NoError(t, err)
	assert.Equal(t, ibcCodeID, cInfo.CodeID)
	assert.Equal(t, cInfo.IBCPortID, types.PortIDForContract(addr))

	_, err = keeper.MigrateContract(ctx, addr, creator, ibcCodeID, []byte("{}"))
	require.NoError(t, err)

	_, err = keeper.GetContractInfo(ctx, addr)
	require.Error(t, err)
}
  1. TestIBC_relayIBCPacket - ensures the ability to relay the packet to the proper channel.

yun-yeo avatar Apr 25 '22 08:04 yun-yeo