classic-core
classic-core copied to clipboard
TIP #49 Upgrade CosmWasm dependency from 0.16 to 1.0
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
- Bump wasmvm version to 1.0.0.
- Check IBC entry points when instantiating or migrating the contract to give a unique port ID.
- 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
-
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))
}
-
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))
}
-
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))
}
-
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)
}
- TestIBC_relayIBCPacket - ensures the ability to relay the packet to the proper channel.