diff --git a/spec/app/ics-027-interchain-accounts/README.md b/spec/app/ics-027-interchain-accounts/README.md index 15605b145..ea835c5a2 100644 --- a/spec/app/ics-027-interchain-accounts/README.md +++ b/spec/app/ics-027-interchain-accounts/README.md @@ -1,6 +1,7 @@ --- ics: 27 title: Interchain Accounts +version: 2 stage: Draft category: IBC/APP requires: 25, 26 @@ -17,7 +18,18 @@ This standard document specifies packet data structure, state machine handling l ### Motivation -ICS-27 Interchain Accounts outlines a cross-chain account management protocol built upon IBC. ICS-27 enabled chains can programmatically create accounts on other ICS-27 enabled chains & control these accounts via IBC transactions (instead of signing with a private key). Interchain accounts retain all of the capabilities of a normal account (i.e. stake, send, vote) but instead are managed by a separate chain via IBC in a way such that the owner account on the controller chain retains full control over any interchain account(s) it registers on host chain(s). +ICS-27 Interchain Accounts outlines a cross-chain account management protocol built upon IBC. ICS-27-enabled chains can programmatically create accounts on other ICS-27-enabled chains & control these accounts via IBC transactions (instead of signing with a private key). Interchain accounts retain all of the capabilities of a normal account (i.e. stake, send, vote) but instead are managed by a separate chain via IBC in a way such that the owner account on the controller chain retains full control over any interchain account(s) it registers on host chain(s). + +#### Version 2 + +The ICS-27 version 2 comes to address the needs of the community and adheres to the requirements defined [here](https://github.com/cosmos/ibc-go/blob/48f69848bb84d9bc396c750eb656f961c7d773ad/docs/requirements/ics27-multiplexed-requirements.md). + +The main differences between the two versions of the ics27 can be outlined as follow: + +- In the ics27-v2, the controller and host account registration process does not rely on channel handshakes. The new protocol version requires only a single unordered channel to manage all communications between the distinct chains. +- For multi message execution, the order and atomiticy of message execution is guaranteed if messages are place in the same ica IBC Tx. +- The controller can setup `n` accounts on the host chain, managed by the same `icaOwnerAddress`, within a single IBC Tx. `n` should be constrained to an upper bound. +- The controller chain can request the execution of `n` msgs destinated to `m`, with `m <= n`, distinct host accounts within a single IBC Tx. ### Definitions @@ -30,534 +42,646 @@ The IBC handler interface & IBC relayer module interface are as defined in [ICS- ### Desired properties -- Permissionless: An interchain account may be created by any actor without the approval of a third party (e.g. chain governance). Note: Individual implementations may implement their own permissioning scheme, however the protocol must not require permissioning from a trusted party to be secure. +- Permissionless: An interchain account may be created by any actor without the approval of a third party (e.g. chain governance). Note: Individual implementations may implement their own permissioning scheme, however, the protocol must not require permissioning from a trusted party to be secure. - Fault isolation: A controller chain must not be able to control accounts registered by other controller chains. For example, in the case of a fork attack on a controller chain, only the interchain accounts registered by the forked chain will be vulnerable. -- The ordering of transactions sent to an interchain account on a host chain must be maintained. Transactions must be executed by an interchain account in the order in which they are sent by the controller chain. -- If a channel closes, the controller chain must be able to regain access to registered interchain accounts by simply opening a new channel. -- Each interchain account is owned by a single account on the controller chain. Only the owner account on the controller chain is authorized to control the interchain account. The controller chain is responsible for enforcing this logic. -- The controller chain must store the account address of any owned interchain accounts registered on host chains. -- A host chain must have the ability to limit interchain account functionality on its chain as necessary (e.g. a host chain can decide that interchain accounts registered on the host chain cannot take part in staking). - -## Technical specification +- The ordering of transactions sent to an interchain account on a host chain must be maintained. Transactions must be executed by an interchain account in the order in which they are sent by the controller chain. +- The controller chain must store the account address of any owned interchain accounts registered on host chains. +- A host chain must have the ability to limit interchain account functionality on its chain as necessary (e.g. a host chain can decide that interchain accounts registered on the host chain cannot take part in staking). This should be achieved with a blacklist mechanisms. +- An icaOwnerAccount on the controller chain can manage 1..n hostAccount(s) on the host chain. A hostAccount on the host chain can be managed by 1 and only 1 icaOwnerAccount on the controller chain. +- Many controller accounts on the same controller chain should be able to send messages to many host accounts on the same host chain through the same channel. ### General design -A chain can utilize one or both parts of the interchain accounts protocol (*controlling* and *hosting*). A controller chain that registers accounts on other host chains (that support interchain accounts) does not necessarily have to allow other controller chains to register accounts on its chain, and vice versa. +The interchain account protocol defines the relationship and interactions between two chains with different roles: the controller chain and the host chain. The protocol allows a controller chain to create and manage accounts on a host chain programmatically through IBC transactions, bypassing the need for private key signatures. -This specification defines the general way to register an interchain account and send tx bytes to be executed on behalf of the owner account. The host chain is responsible for deserializing and executing the tx bytes and the controller chain must know how the host chain will handle the tx bytes in advance of sending a packet, thus this must be negotiated during channel creation. +This specification defines the general way to send TX bytes from a controller chain, on an already established ica-channel, to a host chain. The host chain is responsible for deserializing and executing the tx bytes and the controller chain must know how the host chain will handle the tx bytes in advance of sending a packet, thus this must be negotiated during channel creation. -### Controller chain contract +The ICS-27 version 2 is composed of two subprotocols, namely `icaControlling` and `icaHosting`. The *icaControlling* must be implemented on the controlling chain and is responsible for sending IBC packets to register and manage interchain accounts on the host chain. The *icaHosting* must be implemented on the host chain and is responsible for receiving IBC packets to generate addresses and execute transactions on behalf of the controller chain. -#### **RegisterInterchainAccount** +A chain can utilize one or both subprotocols of the interchain account protocol. A controller chain that registers accounts on other host chains (that support interchain accounts) does not necessarily have to allow other controller chains to register accounts on its chain, and vice versa. -`RegisterInterchainAccount` is the entry point to registering an interchain account. -It generates a new controller portID using the owner account address. -It will bind to the controller portID and -call 04-channel `ChanOpenInit`. An error is returned if the controller portID is already in use. -A `ChannelOpenInit` event is emitted which can be picked up by an offchain process such as a relayer. -The account will be registered during the `OnChanOpenTry` step on the host chain. -This function must be called after an `OPEN` connection is already established with the given connection identifier. -The caller must provide the complete channel version. This MUST include the ICA version with complete metadata and it MAY include -versions of other middleware that is wrapping ICA on both sides of the channel. Note this will require contextual information -on what middleware is enabled on either end of the channel. Thus it is recommended that an ICA-auth application construct the ICA -version automatically and allow for users to optionally enable additional middleware versioning. +## Technical specification -```typescript -function RegisterInterchainAccount(connectionId: Identifier, owner: string, version: string) returns (error) { -} -``` +### Data Structures -#### **SendTx** +We define two types of interchain account packet data the `icaRegisterPacketData` and the `icaExecutePacketData`. Each of the packet data has its data structure. -`SendTx` is used to send an IBC packet containing instructions (messages) to an interchain account on a host chain for a given interchain account owner. +Additionally, we define the `icaPacketDataType` that will be used to distinguish between the two types of packets. ```typescript -function SendTx( - capability: CapabilityKey, - connectionId: Identifier, - portId: Identifier, - icaPacketData: InterchainAccountPacketData, - timeoutTimestamp uint64 -): uint64 { - // check if there is a currently active channel for - // this portId and connectionId, which also implies an - // interchain account has been registered using - // this portId and connectionId - activeChannelID, found = GetActiveChannelID(portId, connectionId) - abortTransactionUnless(found) +enum icaPacketDataType { + REGISTER, + EXECUTE, +} +``` - // validate timeoutTimestamp - abortTransactionUnless(timeoutTimestamp <= currentTimestamp()) +The `icaRegistrationPacketData` contains the parameters `icaOwnerAddress` and the array of `hostAccountIds` that will be passed by the controller chain to the host chain that will be used in conjunction with the `packet.sourcePort` and `packet.sourceChannel` to generate the addresses on the host chain. - // validate icaPacketData - abortTransactionUnless(icaPacketData.type == EXECUTE_TX) - abortTransactionUnless(icaPacketData.data != nil) +```typescript +interface icaRegisterPacketData { + icaType: icaPacketDataType = REGISTER, + icaOwnerAddress : string, + hostAccountIds: [] uint64 // The hostAccountIds to be used for registration within a single tx + // memo: string, // Investigate if we want memo here. +} +``` - // send icaPacketData to the host chain on the active channel - sequence = handler.sendPacket( - capability, - portId, // source port ID - activeChannelID, // source channel ID - 0, - timeoutTimestamp, - protobuf.marshal(icaPacketData) // protobuf-marshalled bytes of packet data - ) +The `icaExecutePacketData` contains the parameters `icaOwnerAddress`, the array of `hostAccountIds`, an array of `msgs` and a `memo` that will be passed by the controller chain to the host chain to execute the msgs on the associated account addresses. - return sequence +```typescript +interface icaExecutePacketData { + icaType: icaPacketDataType = EXECUTE, + icaOwnerAddress : string, + hostAccountIds: [] uint64, + msgs: [] Any, //msg + memo: string, } ``` -### Host chain contract +ICS-27 version 2 defines four acknowledgment data types, namely `icaExecutePacketSuccess`, `icaRegisterPacketSuccess`,`icaRegisterPacketError`, `icaExecutePacketError`. Each of them stores different values that are used in a different flow of the interchain account protocol. -#### **RegisterInterchainAccount** +```typescript +// type icaPacketAcknowledgement = icaExecutePacketSuccess | icaRegisterPacketSuccess | icaRegisterPacketError | icaExecutePacketError; +enum icaPacketAcknowledgementType{ + REGISTER_SUCCESS, + EXECUTE_SUCCESS, + REGISTER_ERROR, + EXECUTE_ERROR, +} +``` -`RegisterInterchainAccount` is called on the `OnChanOpenTry` step during the channel creation handshake. +Whether an interchain account flow fails the reason for failure (if any) will be returned. ```typescript -function RegisterInterchainAccount(counterpartyPortId: Identifier, connectionID: Identifier) returns (nil) { - // checks to make sure the account has not already been registered - // creates a new address on chain deterministically given counterpartyPortId and underlying connectionID - // calls SetInterchainAccountAddress() +interface icaRegisterPacketError { + type: icaPacketAcknowledgementType = REGISTER_ERROR, + error: string } ``` -#### **AuthenticateTx** +```typescript +interface icaExecutePacketError { + type: icaPacketAcknowledgementType = EXECUTE_ERROR, + error: string +} +``` -`AuthenticateTx` is called before `ExecuteTx`. -`AuthenticateTx` checks that the signer of a particular message is the interchain account associated with the counterparty portID of the channel that the IBC packet was sent on. +The `icaRegisterPacketSuccess` is defined to handle the successful registration flow. In the success case, the account address generated on the host chain will be stored in the acknowledgment and delivered back to the controller chain. ```typescript -function AuthenticateTx(msgs []Any, connectionId string, portId string) returns (error) { - // GetInterchainAccountAddress(portId, connectionId) - // if interchainAccountAddress != msgSigner return error +interface icaRegisterPacketSuccess { + type: icaPacketAcknowledgementType = REGISTER_SUCCESS, + hostAccounts: [] string, } ``` -#### **ExecuteTx** - -Executes each message sent by the owner account on the controller chain. +The `icaExecutePacketSuccess` is defined to handle the successful execution flow. In the success case the array of bytes `resultData`, containing the ordered return values of each message execution, will be stored in the acknowledgment and delivered back to the controller chain. ```typescript -function ExecuteTx(sourcePort: Identifier, channel Channel, msgs []Any) returns (resultString, error) { - // validate each message - // retrieve the interchain account for the given channel by passing in source port and channel's connectionID - // verify that interchain account is authorized signer of each message - // execute each message - // return result of transaction +interface icaExecutePacketSuccess { + type: icaPacketAcknowledgementType = EXECUTE_SUCCESS, + resultData: [] bytes, } ``` -### Utility functions +#### **Metadata negotiation** + +The ICS-04 allows for each channel version negotiation to be application-specific. ICS-27 takes advantage of [ICS-04 channel version negotiation](../../core/ics-004-channel-and-packet-semantics/README.md#versioning) to negotiate metadata and channel parameters during the channel handshake. In the case of interchain accounts, the channel version will be a string of a JSON struct containing all the relevant metadata intended to be relayed to the counterparty during the channel handshake steps. The metadata used for the ICS-27 version 2 will contain the encoding format along with the channel version itself. ```typescript -// Sets the active channel for a given portID and connectionID. -function SetActiveChannelID(portId: Identifier, connectionId: Identifier, channelId: Identifier) returns (error){ +version: { + "Version": "ics27-v2", // channel version + "Encoding": "requested_encoding_type", // Json, protobuf.. } +``` -// Returns the ID of the active channel for a given portID and connectionID, if present. -function GetActiveChannelID(portId: Identifier, connectionId: Identifier) returns (Identifier, boolean){ -} +// Note. For now, may be ok like this, but eventually, we can make the encoding a channelEnd native parameter. -// Stores the address of the interchain account in state. -function SetInterchainAccountAddress(portId: Identifier, connectionId: Identifier, address: string) returns (string) { -} +### Transaction Types -// Retrieves the interchain account from state. -function GetInterchainAccountAddress(portId: Identifier, connectionId: Identifier) returns (string, bool){ +The ICS-27 version 2 defines two types of transactions `REGISTER_TX` and `EXECUTE_TX`. + +```typescript +enum icaTxTypes { + REGISTER_TX, + EXECUTE_TX } ``` -### Register & controlling flows +### Sub-protocols -#### Register account flow +#### **icaControlling** -To register an interchain account we require an off-chain process (relayer) to listen for `ChannelOpenInit` events with the capability to finish a channel creation handshake on a given connection. +##### Port & channel setup -1. The controller chain binds a new IBC port with the controller portID for a given *interchain account owner address*. +The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialised) to bind to the appropriate port (owned by the module). -This port will be used to create channels between the controller & host chain for a specific owner/interchain account pair. Only the account with `{owner-account-address}` matching the bound port will be authorized to send IBC packets over channels created with the controller portID. It is up to each controller chain to enforce this port registration and access on the controller side. +Once the `setup` function has been called, channels can be created through the IBC routing module between instances of the interchain account module on separate chains. -2. The controller chain emits an event signaling to open a new channel on this port given a connection. -3. A relayer listening for `ChannelOpenInit` events will continue the channel creation handshake. -4. During the `OnChanOpenTry` callback on the host chain an interchain account will be registered and a mapping of the interchain account address to the owner account address will be stored in state (this is used for authenticating transactions on the host chain at execution time). -5. During the `OnChanOpenAck` callback on the controller chain a record of the interchain account address registered on the host chain during `OnChanOpenTry` is set in state with a mapping from (controller portID, controller connectionID) -> interchain account address. See [metadata negotiation](#metadata-negotiation) section below for how to implement this. -6. During the `OnChanOpenAck` & `OnChanOpenConfirm` callbacks on the controller & host chains respectively, the [active-channel](#active-channels) for this interchain account/owner pair, is set in state. +An administrator (with the permissions to create connections & channels on the state machine) is responsible for setting up connections to other state machines & creating channels to other instances of this module (or another module supporting this interface) on other chains. This specification defines packet handling semantics only and defines them in such a fashion that the module itself doesn't need to worry about what connections or channels might or might not exist at any point in time. -#### Active channels +```typescript +function setup() { + capability = routingModule.bindPort("ica-controller", ModuleCallbacks{ + onChanOpenInit, + onChanOpenTry, // Force Abort + onChanOpenAck, + onChanOpenConfirm, // Force Abort + onChanCloseInit, // Force Abort + onChanCloseConfirm, // Do Nothing + // TODO Missing Upgrade Callbacks + onRecvPacket, // Force Abort + onTimeoutPacket, + onAcknowledgePacket, + }) + claimCapability("port", capability) +} +``` + +##### Routing module callbacks + +The routing module callbacks associated with the channel management are at the base of the interchain account protocol. By design, only the controller chain can start a channel creation handshake. + +###### Channel lifecycle management -The controller and host chain must keep track of an `active-channel` for each registered interchain account. The `active-channel` is set during the channel creation handshake process. This is a safety mechanism that allows a controller chain to regain access to an interchain account on a host chain in case of a channel closing. +When machine `A`, with the role of controller, starts a new channel handshake, chain `B` with the role of host must accept the new channel if and only if: -An example of an active channel on the controller chain can look like this: +- The channel being created is `UNORDERED`. +- The counterpartyMetatada.Version string is `ics27-v2`. +- The counterpartyPortIdentifier is `ica-controller`. ```typescript -{ - // Controller Chain - SourcePortId: `icacontroller-`, - SourceChannelId: ``, - // Host Chain - CounterpartyPortId: `icahost`, - CounterpartyChannelId: ``, +function onChanOpenInit( + order: ChannelOrder, + portIdentifier: Identifier, + channelIdentifier: Identifier, + counterpartyPortIdentifier: Identifier, + counterpartyChannelIdentifier: Identifier, + version: string) => (version: string, err: Error) { + // only unordered channels allowed + abortTransactionUnless(order === UNORDERED) + abortTransactionUnless(portIdentifier === "ica-controller") + abortTransactionUnless(counterpartyPortIdentifier === "ica-host") + + if version == "" { + // default to latest supported version + metadata = { + Version: "ics27-v2", + Encoding: DefaultEncoding, //decide default econding + } + version = marshalJSON(metadata) + return version, nil + } else{ + // If the version is not empty and is among those supported, we return the version + metadata = UnmarshalJSON(version) + // assert that version is "ica" + abortTransactionUnless(matadata.Version === "ics27-v2") + // assert the choosed encoding is supported. + abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) + return version, nil + } } ``` -In the event of a channel closing, the active channel may be replaced by starting a new channel handshake with the same port identifiers on the same underlying connection of the original active channel. ICS-27 channels can only be closed in the event of a timeout (if the implementation uses ordered channels) or in the unlikely event of a light client attack. Controller chains must retain the ability to open new ICS-27 channels and reset the active channel for a particular portID (containing `{owner-account-address}`) and connectionID pair. +```typescript +function onChanOpenTry( + order: ChannelOrder, + portIdentifier: Identifier, + channelIdentifier: Identifier, + counterpartyPortIdentifier: Identifier, + counterpartyChannelIdentifier: Identifier, + counterpartyVersion: string) => (version: string, err: Error) { + // Always Abort + abortTransactionUnless(false, "Invalid channel creation flow: channel handshake must be initiated by controller chain") +} +``` -The controller and host chains must verify that any new channel maintains the same metadata as the previous active channel to ensure that the parameters of the interchain account remain the same even after replacing the active channel. The `Address` of the metadata should not be verified since it is expected to be empty at the INIT stage, and the host chain will regenerate the exact same address on TRY, because it is expected to generate the interchain account address deterministically from the controller portID and connectionID (both of which must remain the same). +```typescript +function onChanOpenAck( + portIdentifier: Identifier, + channelIdentifier: Identifier, + counterpartyChannelIdentifier: Identifier, + counterpartyVersion: string) { + // port has already been validated + // assert that counterparty selected version is the same as our version + counterpartyMetadata = UnmarshalJSON(CounterpartyVersion) + channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) + channelMetadata=UnmarshalJSON(channel.version) + abortTransactionUnless(counterpartyMetadata.Version === "ics27-v2") + // Check if the econding has been agreed + abortTransactionUnless(counterpartyMetadata.Encoding === channelMetadata.Encoding) +} +``` -#### **Metadata negotiation** +```typescript +function onChanOpenConfirm( + portIdentifier: Identifier, + channelIdentifier: Identifier) { + // Always abort + abortTransactionUnless(false, "Invalid channel creation flow: channel handshake must be initiated by controller chain") + } +``` -ICS-27 takes advantage of [ICS-04 channel version negotiation](../../core/ics-004-channel-and-packet-semantics/README.md#versioning) to negotiate metadata and channel parameters during the channel handshake. The metadata will contain the encoding format along with the transaction type so that the counterparties can agree on the structure and encoding of the interchain transactions. The metadata sent from the host chain on the TRY step will also contain the interchain account address, so that it can be relayed to the controller chain. At the end of the channel handshake, both the controller and host chains will store a mapping of (controller chain portID, controller/host connectionID) to the newly registered interchain account address ([account registration flow](#register-account-flow)). +```typescript +// Channel closure is disabled. +function onChanCloseInit( + portIdentifier: Identifier, + channelIdentifier: Identifier) { + // always abort transaction + abortTransactionUnless(false, "Invalid flow: channel closure is disabled") +} +``` -ICS-04 allows for each channel version negotiation to be application-specific. In the case of interchain accounts, the channel version will be a string of a JSON struct containing all the relevant metadata intended to be relayed to the counterparty during the channel handshake step ([see summary below](#metadata-negotiation-summary)). +```typescript +function onChanCloseConfirm( + portIdentifier: Identifier, + channelIdentifier: Identifier) { + // no action necessary, this cannot be reached. +} +``` + +##### Interchain Account EntryPoints -Combined with the one channel per interchain account approach, this method of metadata negotiation allows us to pass the address of the interchain account back to the controller chain and create a mapping from (controller portID, controller connection ID) -> interchain account address during the `OnChanOpenAck` callback. As outlined in the [controlling flow](#controlling-flow), a controller chain will need to know the address of a registered interchain account in order to send transactions to the account on the host chain. +To interact with the interchain account protocol, the user can generate two types of tx, namely `REGISTER_TX` and `EXECUTE_TX`. For each Tx type, we define a icaTxHandler so that we have `icaRegisterTxHandler` and `icaExecuteTxHandler`. Both tx handlers must verify, that the signer is the `icaOwnerAddress` and then, based on the `icaTxType`, they must call the associated functions that will construct the related kind of packet. -#### **Metadata negotiation summary** +```typescript +function icaRegisterTxHandler(portId: string, channelId: string, timeoutTimestamp: uint64, icaOwnerAddress: string, hostAccountNumber: unit64): uint64 { -`interchain-account-address` is the address of the interchain account registered on the host chain by the controller chain. + // Ensure the tx has been dispatched to the correct handler + abortTransactionUnless(this.Tx.type===REGISTER_TX) + // verify Tx Signature:: must be signed by icaOwnerAddress + abortTransactionUnlesss(IsValidAddress(icaOwnerAddress)) + abortTransactionUnless(this.Tx.signer===icaOwnerAddress) // CHECK PROPER SYNTAX + // Validate functions parameter.. -- **INIT** + // Should compute and pass in timeout related things or this should be done in sendRegisterTx? + return sequence = sendRegisterTx(portId, channelId, timeoutTimestamp, icaOwnerAddress, hostAccountNumber) +} +``` -Initiator: Controller +```typescript +function icaExecuteTxHandler(portId: string, channelId: string, timeoutTimestamp: uint64, icaOwnerAddress: string, hostAccountIds:[] unit64, msgs: []msgs, memo:string ) : uint64 { + + // Ensure the tx has been dispatched to the correct handler + abortTransactionUnless(this.Tx.type===EXECUTE_TX) + // verify Tx Signature:: must be signed by icaOwnerAddress + abortTransactionUnlesss(IsValidAddress(icaOwnerAddress)) + abortTransactionUnless(this.Tx.signer===icaOwnerAddress)// CHECK PROPER SYNTAX + // Validate functions parameter.. + + // call sendExecuteTx + // Should compute and pass in timeout related things or this should be done in sendExecuteTx? + return sequence= sendExecuteTx(portId, channelId, timeoutTimestamp, icaOwnerAddress, hostAccountIds, msgs, memo) +} +``` -Datagram: ChanOpenInit +##### Module State -Chain Acted Upon: Controller +The interchain account module keeps track of the controlled `hostAccounts` associated with particular channels in the state. Additionally, it tracks the `nextHostAccountId` and the `unusedHostAccountsIds` array. Fields of the `ModuleState` are assumed to be in scope. -Version: +When the protocol is initiated, the `unusedHostAccountIds` array is empty. So the Ids are initially associated with the `nextHostAccountId` which starts at 0 and is increased linearly as new accounts get registered. When a register message fails, the Ids computed during the sending are recovered and inserted in the `unusedHostAccountIds` array during the acknowlegment. So in the next register message, the Ids in `unusedHostAccountIds` are the first used to generate the new accounts. +// TODO There is space to easily add a delete account mechanism -```json -{ - "Version": "ics27-1", - "ControllerConnectionId": "self_connection_id", - "HostConnectionId": "counterparty_connection_id", - "Address": "", - "Encoding": "requested_encoding_type", - "TxType": "requested_tx_type", +```typescript +interface ModuleState { + hostAccounts: Map>>>, + nextHostAccountId : uint64, + unusedHostAccountIds: []uint64 } ``` -Comments: The address is left empty since this will be generated and relayed back by the host chain. The connection identifiers must be included to ensure that if a new channel needs to be opened (in case active channel times out), then we can ensure that the new channel is opened on the same connection. This will ensure that the interchain account is always connected to the same counterparty chain. +##### Utility functions -- **TRY** +```typescript -Initiator: Relayer +// Stores the address of the interchain account in state. +function setInterchainAccountAddress(portId: string, channelId: string, icaOwnerAccount: string, hostAccountId: uint64, address: string) : string { + +hostAccounts[portId][channelId][icaOwnerAccount][hostAccountId].hostAccountAddress=address -Datagram: ChanOpenTry +return address +} -Chain Acted Upon: Host +// Retrieves the interchain account address from state. +function getInterchainAccountAddress(portId: string, channelId: string,icaOwnerAddress: string, hostAccountId: uint64) : string { -Version: +return hostAccounts[portId][channelId][icaOwnerAddress][hostAccountId].hostAccountAddress -```json -{ - "Version": "ics27-1", - "ControllerConnectionId": "counterparty_connection_id", - "HostConnectionId": "self_connection_id", - "Address": "interchain_account_address", - "Encoding": "negotiated_encoding_type", - "TxType": "negotiated_tx_type", } ``` -Comments: The ICS-27 application on the host chain is responsible for returning this version given the counterparty version set by the controller chain in INIT. The host chain must agree with the single encoding type and a single tx type that is requested by the controller chain (ie. included in counterparty version). If the requested encoding or tx type is not supported, then the host chain must return an error and abort the handshake. -The host chain must also generate the interchain account address and populate the address field in the version with the interchain account address string. +##### Helper Functions -- **ACK** +The helper functions described herein must be implemented in a controlling interchain account module. -Initiator: Relayer +###### **registerInterchainAccount** -Datagram: ChanOpenAck +When a user on the controller chain wants to register new interchain accounts, he will send a Tx whose type is `REGISTER_TX`, including the parameter `hostAccountNumber` to specify the number of accounts to register, which will trigger the `registerInterchainAccount` function logic to be executed on the controller chain. The `registerInterchainAccount` function select the `usedHostAccountIds` either from the list of `unusedHostAccountIDs`, if non empty, or from the `nextHostAccountId` parameters in the moduleState. -Chain Acted Upon: Controller +The host chain Must be able to generate and store in state the hostAccountAddress, which will be controlled by the icaOwnerAddress by using the information provided about the hostAccounts passed in the `REGISTER_TX` and must pass back the generated address inside the ack. Once received the ack, the controller chain must store the `hostAccountAddress` generated address in its module state. In the case of an error during the registration, the `usedHostAccountIds` will be added to the `unusedHostAccountIDs` array. -CounterpartyVersion: +```typescript +function registerInterchainAccount( + portId: string, + channelId: string, + icaOwnerAddress: string, + hostAccountNumber: uint64 // The number of accounts the icaOwnerAddress wants to register within a single tx + ) + : ([]hostAccountIds,err) { + + for i in 0..hostAccountNumber{ + let hostAccountId: uint64 + if(unusedHostAccountIds.length > 0) { + // Use an unused ID if available + hostAccountId = unusedHostAccountIds.pop(); + } else { + // Otherwise, use the nextHostAccountId + hostAccountId = nextHostAccountId; + nextHostAccountId++; + } + // Use hostAccountId to initialize the account + //err=InitInterchainAccountAddress(portId, channelId, icaOwnerAddress, hostAccountId) + if(getInterchainAccountAddress(portId,channelId,icaOwnerAddress,hostAccountId)!=="") { + error={"intechainAccountKeys already used"} + return ([],error) + } + //abortTransactionUnless(err!==nil) + // Push into hostSequenceNumber array the used nextHostAccountId + // We will return this array to keep track of the used sequence numbers + hostAccountIds.push(hostAccountId) + } -```json -{ - "Version": "ics27-1", - "ControllerConnectionId": "self_connection_id", - "HostConnectionId": "counterparty_connection_id", - "Address": "interchain_account_address", - "Encoding": "negotiated_encoding_type", - "TxType": "negotiated_tx_type", + return hostAccountIds,nil } ``` -Comments: On the ChanOpenAck step, the ICS27 application on the controller chain must verify the version string chosen by the host chain on ChanOpenTry. The controller chain must verify that it can support the negotiated encoding and tx type selected by the host chain. If either is unsupported, then it must return an error and abort the handshake. -If both are supported, then the controller chain must store a mapping from the channel's portID to the provided interchain account address and return successfully. +##### Packet relay -#### Controlling flow +`sendRegisterTx` and `sendExecuteTx` must be called by a transaction handler in the controller chain module which performs appropriate signature checks. In particular, the transaction handlers must verify that the `icaOwnerAddress` is the actual signer of the TX. -Once an interchain account is registered on the host chain a controller chain can begin sending instructions (messages) to the host chain to control the account. +//TODO CLARIFY WAY BETTER THE CONCEPT // May be in a different section +Thinking about a smart contract system, then the system should verify that the tx the user generates to call the `sendRegisterTx` and `sendExecuteTx` contract function has been signed by the `icaOwnerAddress`. -1. The controller chain calls `SendTx` and passes message(s) that will be executed on the host side by the associated interchain account (determined by the controller side port identifier) +`sendRegisterTx` is used by a controller chain to send an IBC packet containing instructions on the number of host accounts to create on a host chain for a given interchain account owner. -Cosmos SDK pseudo-code example: +```typescript +function sendRegisterTx( + sourcePort: string, + sourceChannel: string, + tiemoutTimestamp: uint64, // in unix nanoseconds + icaOwnerAddress: string, + hostAccountNumber: uint64, // Account number for which we are requesting the generation + //memo: string, // Do we want to allow memo to be used in here? Probably we should not + +) : uint64 { + + // retrieve channel + channel = provableStore.get(channelPath(sourcePort, sourceChannel)) + metadata=UnmarshalJson(channel.version) + abortTransactionUnless(metadata.Version=="ics27-v2") + abortTransactionUnless(sourcePort=="ica-controller") -```golang -// connectionId is the identifier for the controller connection -interchainAccountAddress := GetInterchainAccountAddress(portId, connectionId) -msg := &banktypes.MsgSend{FromAddress: interchainAccountAddress, ToAddress: ToAddress, Amount: amount} -icaPacketData = InterchainAccountPacketData{ - Type: types.EXECUTE_TX, - Data: serialize(msg), - Memo: "memo", -} + // validate timeoutTimestamp + abortTransactionUnless(timeoutTimestamp > currentTimestamp()) -// Sends the message to the host chain, where it will eventually be executed -SendTx(ownerAddress, connectionId, portID, data, timeout) -``` + let err : error = nil + let usedhostAccountIds : []uint64 = [] -2. The host chain upon receiving the IBC packet will call `DeserializeTx`. - -3. The host chain will then call `AuthenticateTx` and `ExecuteTx` for each message and return an acknowledgment containing a success or error. + abortTransactionUnless(hostAccountNumber > 0) + + // A single registration message enable the registration of n accounts. + // A potential limit of the number of accounts to guarantee a non out-of-gas error is to be discussed + usedHostAccountIds,err = registerInterchainAccount(sourcePort, sourceChannel, icaOwnerAddress,hostAccountNumber) + + abortTransactionUnless(err===nil) + + //sendRegisterTx has been called by a transaction handler in the controller chain module which has performed appropriate signature checks for the icaOwnerAddress such that we can assume the tx has already been validated for being originated by the icaOwnerAddress -Messages are authenticated on the host chain by taking the controller side port identifier and calling `GetInterchainAccountAddress(controllerPortId, hostConnectionId)` to get the expected interchain account address for the current controller port and connection identifier. If the signer of this message does not match the expected account address then authentication will fail. + icaPacketData = icaRegistrationPacketData{icaOwnerAccount,usedHostAccountIds} + + // send packet using the interface defined in ICS4 + sequence = handler.sendPacket( + getCapability("port"), + sourcePort, + sourceChannel, + 0, // timeoutHeight + timeoutTimestamp, + protobuf.marshal(icaPacketData) // protobuf-marshalled bytes of packet data + ) + //Emit Event + return sequence +} +``` -### Packet Data +`sendExecuteTx` is used by a controller chain to send an IBC packet containing instructions (messages) and the host accounts references that should execute the tx on behalf of the interchain account owner. -`InterchainAccountPacketData` contains an array of messages that an interchain account can execute and a memo string that is sent to the host chain as well as the packet `type`. ICS-27 version 1 has only one type `EXECUTE_TX`. +```typescript +function sendExecuteTx( + sourcePort: string, + sourceChannel: string, + timeoutTimestamp: uint64, // in unix nanoseconds + icaOwnerAddress: string, + hostAccountIds: [] uint64, // TODO Reason about this. Maybe could use addresses directly + msgs: []msg, + memo: string) + : uint64 { + + // retrieve channel + channel = provableStore.get(channelPath(sourcePort, sourceChannel)) + metadata=UnmarshalJson(channel.version) + abortTransactionUnless(metadata.Version=="ics27-v2") + abortTransactionUnless(sourcePort=="ica-controller") + + // Verify that the provided hostAccountIds match with an already registered hostAccountAddress + for seq in hostAccountIds{ + abortTransactionUnless(getInterchainAccountAddress(sourcePort, sourceChannel icaOwnerAddress,seq)!=="", "Interchain Account Not Registered") + } + + // It exist at least one message to be executed + abortTransactionUnless(msgs.isNotEmpty()) + + icaPacketData = IcaExecutePacketData{icaOwnerAddress,hostAccountsIds,msgs,memo} -```proto -message InterchainAccountPacketData { - enum type - bytes data = 1; - string memo = 2; + // send packet using the interface defined in ICS4 + sequence = handler.sendPacket( + getCapability("port"), + sourcePort, + sourceChannel, + 0, // timeoutHeight + timeoutTimestamp, + protobuf.marshal(icaPacketData) // protobuf-marshalled bytes of packet data + ) + //Emit Event + return sequence } ``` -The acknowledgment packet structure is defined as in [ics4](https://github.com/cosmos/ibc-go/blob/main/proto/ibc/core/channel/v1/channel.proto#L135-L148). If an error occurs on the host chain the acknowledgment contains the error message. +Note that interchain accounts controller modules should not execute any logic upon packet receipt, i.e. the `onRecvPacket` callback should not be called, and in case it is called, it should simply return an error acknowledgment: -```proto -message Acknowledgement { - // response contains either a result or an error and must be non-empty - oneof response { - bytes result = 21; - string error = 22; - } +```typescript +// Called on Controller Chain by Relayer +function onRecvPacket(packet Packet) { + return NewErrorAcknowledgement(ErrInvalidChannelFlow) } ``` -### Custom logic +`onAcknowledgePacket` is called by the routing module when a packet sent by this module has been acknowledged. -ICS-27 relies on [ICS-30 middleware architecture](../ics-030-middleware) to provide the option for application developers to apply custom logic on the success or fail of ICS-27 packets. +```typescript +// Called on Controller Chain by Relayer +function onAcknowledgePacket( + packet: Packet, + acknowledgement: bytes) { + ack=Deserialize(acknowledgement) + switch ack.type: + case types.REGISTER_SUCCESS: + + // We have to store the addresses into our module state mapping + let i :uint64 = 0 + // Store addresses under the right sequence number. + for address in ack.hostAccountAddress{ + // Store in hostAccounts module state the address returned in the ack + hostAccounts[packet.sourcePort][packet.sourceChannel][packet.data.icaOwnerAddress][packet.data.hostAccountIds.getElement(i)].hostAccountAddress=address + // Increment the positional number + i=i+1 + } + //Emit Event with the addresses generated by the host chain + + case types.REGISTER_ERROR: + // In case of registration errors, populate the unusedHostAccountsIds with the ids provided for this registering Tx. The usage of the unusedHostAccountIds serves to deter potential gaps in the sequence of account IDs + for id in packet.data.hostAccountIds { + unusedHostAccountIds.push(id); + } + //Emit Event + case types.EXECUTE_SUCCESS: + //Emit Event with resultsData In theory no state changes happened on controller chain, so no extra op is required + // No Op + case types.EXECUTE_ERROR: + //Emit Event. In theory no state changes happened on controller chain, so no extra op is required + // No Op + + } +``` + +```typescript +// Called on Controller Chain by Relayer +function onTimeoutPacket(packet: Packet) { -Controller chains will wrap `OnAcknowledgementPacket` & `OnTimeoutPacket` to handle the success or fail cases for ICS-27 packets. + switch packet.data.icaType { + case types.REGISTER: + for id in packet.data.hostAccountIds { + unusedHostAccountIds.push(id); + } + //Emit Event + case types.EXECUTE: + //Emit Event + // No Op + } +} +``` -### Port & channel setup +#### *icaHosting* -The interchain account module on a host chain must always bind to a port with the id `icahost`. Controller chains will bind to ports dynamically, as specified in the identifier format [section](#identifier-formats). +##### Port & channel setup -The example below assumes a module is implementing the entire `InterchainAccountModule` interface. The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialized) to bind to the appropriate port. +Here we define the setup function for the *icaHosting* subprotocol. ```typescript function setup() { - capability = routingModule.bindPort("icahost", ModuleCallbacks{ - onChanOpenInit, + capability = routingModule.bindPort("ica-host", ModuleCallbacks{ + onChanOpenInit, // Force Abort onChanOpenTry, - onChanOpenAck, - onChanOpenConfirm, - onChanCloseInit, - onChanCloseConfirm, - onChanUpgradeInit, // read-only - onChanUpgradeTry, // read-only - onChanUpgradeAck, // read-only - onChanUpgradeOpen, + onChanOpenAck, // Force Abort + onChanOpenConfirm, + onChanCloseInit, // Force Abort + onChanCloseConfirm, // Do nothing + // TODO Missing Upgrade Callbacks onRecvPacket, - onTimeoutPacket, - onAcknowledgePacket, - onTimeoutPacketClose + //onTimeoutPacket, // Force Abort + //onAcknowledgePacket, // Force Abort }) claimCapability("port", capability) } ``` -Once the `setup` function has been called, channels can be created via the IBC routing module. +##### Routing module callbacks -### Channel lifecycle management +The routing module callbacks associated with the channel management are at the base of the interchain account protocol. By design, the host chain can only accept/negotiate a channel creation handshake started by a controller chain. -An interchain account module will accept new channels from any module on another machine, if and only if the channel initialization step is being invoked from the controller chain. +###### Channel lifecycle management + +When machine `A`, with the role of controller, starts a new channel handshake, chain `B` with the role of host must accept the new channel if and only if: + +- The channel being created is `UNORDERED`. +- The counterpartyMetatada.Version string is `ics27-v2`. +- The counterpartyPortIdentifier is `ica-controller`. ```typescript -// Called on Controller Chain by InitInterchainAccount function onChanOpenInit( order: ChannelOrder, - connectionHops: [Identifier], portIdentifier: Identifier, channelIdentifier: Identifier, counterpartyPortIdentifier: Identifier, counterpartyChannelIdentifier: Identifier, - version: string -): (version: string, err: Error) { - // validate port format - abortTransactionUnless(validateControllerPortParams(portIdentifier)) - // only allow channels to be created on the "icahost" port on the counterparty chain - abortTransactionUnless(counterpartyPortIdentifier === "icahost") - - // retrieve channel and connection to access connection ID and counterparty connection ID - channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) - connectionId = channel.connectionHops[0] - connection = provableStore.get(connectionPath(connectionId)) - - if version != "" { - // validate metadata - metadata = UnmarshalJSON(version) - abortTransactionUnless(metadata.Version === "ics27-1") - // all elements in encoding list and tx type list must be supported - abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) - abortTransactionUnless(IsSupportedTxType(metadata.TxType)) - abortTransactionUnless(metadata.ControllerConnectionId === connectionId) - abortTransactionUnless(metadata.HostConnectionId === connection.counterpartyConnectionIdentifier) - } else { - // construct default metadata - metadata = { - Version: "ics27-1", - ControllerConnectionId: connectionId, - HostConnectionId: counterpartyConnectionId, - // implementation may choose a default encoding and TxType - // e.g. DefaultEncoding=protobuf, DefaultTxType=sdk.MultiMsg - Encoding: DefaultEncoding, - TxType: DefaultTxType, - } - version = marshalJSON(metadata) - } - - // only open the channel if: - // - there is no active channel already set (with status OPEN) - // OR - // - there is already an active channel (with status CLOSED) AND - // the metadata matches exactly the existing metadata in the - // version string of the active channel AND the ordering of the - // new channel matches the ordering of the active channel. - activeChannelId, activeChannelFound = GetActiveChannelID(portId, connectionId) - if activeChannelFound { - activeChannel = provableStore.get(channelPath(portId, activeChannelId)) - abortTransactionUnless(channel !== null) - abortTransactionUnless(activeChannel.state === CLOSED) - previousOrder = activeChannel.order - abortTransactionUnless(previousOrder === order) - previousMetadata = UnmarshalJSON(activeChannel.version) - abortTransactionUnless(previousMetadata === metadata) - } - - return version, nil + version: string) => (version: string, err: Error) { + // Always Abort + abortTransactionUnless(false, "Invalid channel creation flow: channel handshake must be initiated by controller chain") } ``` ```typescript -// Called on Host Chain by Relayer function onChanOpenTry( order: ChannelOrder, - connectionHops: [Identifier], portIdentifier: Identifier, channelIdentifier: Identifier, counterpartyPortIdentifier: Identifier, counterpartyChannelIdentifier: Identifier, - counterpartyVersion: string -): (version: string, err: Error) { - // validate port ID - abortTransactionUnless(portIdentifier === "icahost") - - // retrieve channel and connection to access connection ID and counterparty connection ID - channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) - connectionId = channel.connectionHops[0] - connection = provableStore.get(connectionPath(connectionId)) - - // create the interchain account with the counterpartyPortIdentifier - // and the underlying connectionID on the host chain. - address = RegisterInterchainAccount(counterpartyPortIdentifier, connectionId) - - // state change to keep track of successfully registered interchain account - SetInterchainAccountAddress(counterpartyPortIdentifier, connectionId, address) - - cpMetadata = UnmarshalJSON(counterpartyVersion) - // it's not mandatory for the controller to fill in the host connection ID, since - // it could not be possible for it to know it. ibc-go's implementation of the - // controller does fill it in, but an CosmWasm controller implementation would - // not be able. For that reason, the host fills in here its own connection ID. - cpMetadata.HostConnectionId = connectionId - - abortTransactionUnless(cpMetadata.Version === "ics27-1") - // If encoding or txType requested by initializing chain is not supported by host chain then - // fail handshake and abort transaction - abortTransactionUnless(IsSupportedEncoding(cpMetadata.Encoding)) - abortTransactionUnless(IsSupportedTxType(cpMetadata.TxType)) - abortTransactionUnless(cpMetadata.ControllerConnectionId === connection.counterpartyConnectionIdentifier) - abortTransactionUnless(cpMetadata.HostConnectionId === connectionId) + counterpartyVersion: string) => (version: string, err: Error) { - metadata = { - "Version": "ics27-1", - "ControllerConnectionId": cpMetadata.ControllerConnectionId, - "HostConnectionId": cpMetadata.HostConnectionId, - "Address": address, - "Encoding": cpMetadata.Encoding, - "TxType": cpMetadata.TxType, - } - - return string(MarshalJSON(metadata)), nil + abortTransactionUnless(order === UNORDERED) + // Unmarshal metada from counterpartyVersion + counterpartyMetadata = UnmarshalJSON(counterpartyVersion) + + abortTransactionUnless(portIdentifier === "ica-host") + abortTransactionUnless(counterpartyPortIdentifier === "ica-controller") + // assert that version is "ics27-v2" + abortTransactionUnless(counterpartyMetadata.Version === "ics27-v2") + // assert the choosed encoding is supported. + abortTransactionUnless(IsSupportedEncoding(counterpartyMetadata.Encoding)) + return counterpartyVersion, nil } ``` ```typescript -// Called on Controller Chain by Relayer function onChanOpenAck( portIdentifier: Identifier, channelIdentifier: Identifier, - counterpartyChannelIdentifier, - counterpartyVersion: string -) { - // retrieve channel and connection to access connection ID and counterparty connection ID - channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) - connectionId = channel.connectionHops[0] - connection = provableStore.get(connectionPath(connectionId)) - - // validate counterparty metadata decided by host chain - metadata = UnmarshalJSON(version) - abortTransactionUnless(metadata.Version === "ics27-1") - abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) - abortTransactionUnless(IsSupportedTxType(metadata.TxType)) - abortTransactionUnless(metadata.ControllerConnectionId === connectionId) - abortTransactionUnless(metadata.HostConnectionId === connection.counterpartyConnectionIdentifier) - - // state change to keep track of successfully registered interchain account - SetInterchainAccountAddress(portID, metadata.ControllerConnectionId, metadata.Address) - // set the active channel for this owner/interchain account pair - SetActiveChannelID(portIdentifier, metadata.ControllerConnectionId, channelIdentifier) + counterpartyChannelIdentifier: Identifier, + counterpartyVersion: string) { + // Always Abort + abortTransactionUnless(false, "Invalid channel creation flow: channel handshake must be initiated by controller chain") } ``` ```typescript -// Called on Host Chain by Relayer function onChanOpenConfirm( portIdentifier: Identifier, - channelIdentifier: Identifier -) { - channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) - abortTransactionUnless(channel !== null) - - // set the active channel for this owner/interchain account pair - SetActiveChannelID(channel.counterpartyPortIdentifier, channel.connectionHops[0], channelIdentifier) -} -``` - -```typescript -// The controller portID must have the format: `icacontroller-{ownerAddress}` -function validateControllerPortParams(portIdentifier: Identifier) { - split(portIdentifier, "-") - abortTransactionUnless(portIdentifier[0] === "icacontroller") - abortTransactionUnless(IsValidAddress(portIdentifier[1])) + channelIdentifier: Identifier) { + // accept channel confirmations, port has already been validated, version has already been validated } ``` -### Closing handshake - ```typescript +// Channel closure is disabled. function onChanCloseInit( portIdentifier: Identifier, channelIdentifier: Identifier) { - // disallow user-initiated channel closing for interchain account channels - abortTransactionUnless(FALSE) + // always abort transaction + abortTransactionUnless(false, "Invalid flow: channel closure is disabled") } ``` @@ -565,234 +689,413 @@ function onChanCloseInit( function onChanCloseConfirm( portIdentifier: Identifier, channelIdentifier: Identifier) { + // no action necessary } ``` -### Upgrade handshake +##### Module State Host Chain + +The interchain account module on the host chain must track the `hostAccounts` and should track the blacklist of msgs associated with a particular controller account in the state. Fields of the `ModuleState` are assumed to be in scope. ```typescript -// Called on Controller Chain by Authority -function onChanUpgradeInit( - portIdentifier: Identifier, - channelIdentifier: Identifier, - order: ChannelOrder, - connectionHops: [Identifier], - upgradeSequence: uint64, - version: string -): (version: string, err: Error) { - // new version proposed in the upgrade - abortTransactionUnless(version !== "") - metadata = UnmarshalJSON(version) - - // retrieve the existing channel version. - // In ibc-go, for example, this is done using the GetAppVersion - // function of the ICS4Wrapper interface. - // See https://github.com/cosmos/ibc-go/blob/ac6300bd857cd2bd6915ae51e67c92848cbfb086/modules/core/05-port/types/module.go#L128-L132 - channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) - abortTransactionUnless(channel !== null) - currentMetadata = UnmarshalJSON(channel.version) - - // validate metadata - abortTransactionUnless(metadata.Version === "ics27-1") - // all elements in encoding list and tx type list must be supported - abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) - abortTransactionUnless(IsSupportedTxType(metadata.TxType)) - - // the interchain account address on the host chain - // must remain the same after the upgrade. - abortTransactionUnless(currentMetadata.Address === metadata.Address) - - // at the moment it is not supported to perform upgrades that - // change the connection ID of the controller or host chains. - // therefore these connection IDs much remain the same as before. - abortTransactionUnless(currentMetadata.ControllerConnectionId === metadata.ControllerConnectionId) - abortTransactionUnless(currentMetadata.HostConnectionId === metadata.HostConnectionId) - // the proposed connection hop must not change - abortTransactionUnless(currentMetadata.ControllerConnectionId === connectionHops[0]) - - return version, nil +interface ModuleState { + hostAccounts: Map>>>, + // Generic msg blacklist. It can be icaOwnerAddress specific, or only portId,channelId specific. + msgBlacklist: Map>>, + } ``` -```typescript -// Called on Host Chain by Relayer -function onChanUpgradeTry( - portIdentifier: Identifier, - channelIdentifier: Identifier, - order: ChannelOrder, - connectionHops: [Identifier], - upgradeSequence: uint64, - counterpartyPortIdentifier: Identifier, - counterpartyChannelIdentifier: Identifier, - counterpartyVersion: string -): (version: string, err: Error) { - // validate port ID - abortTransactionUnless(portIdentifier === "icahost") - - // upgrade version proposed by counterparty - abortTransactionUnless(counterpartyVersion !== "") - metadata = UnmarshalJSON(counterpartyVersion) - - // retrieve the existing channel version. - // In ibc-go, for example, this is done using the GetAppVersion - // function of the ICS4Wrapper interface. - // See https://github.com/cosmos/ibc-go/blob/ac6300bd857cd2bd6915ae51e67c92848cbfb086/modules/core/05-port/types/module.go#L128-L132 - channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) - abortTransactionUnless(channel !== null) - currentMetadata = UnmarshalJSON(channel.version) +##### Utility functions - // validate metadata - abortTransactionUnless(metadata.Version === "ics27-1") - // all elements in encoding list and tx type list must be supported - abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) - abortTransactionUnless(IsSupportedTxType(metadata.TxType)) +```typescript +// Stores the address of the interchain account in state. +function setInterchainAccountAddress(portId: string, channelId: string, icaOwnerAccount: string, hostAccountId: uint64) : string { +// While the setInterchainAccountAddress of the controller chain only stores the passed in addresses, in the hosting subprotocol this function has to generate new addresses deterministically based on the passed-in parameters and then store them in the module state in the hostAccounts map. +address=newAddress(portId,channelId, icaOwnerAccount, seq) +// Set in the host chain module state the generated address +hostAccounts[portId][channelId][icaOwnerAccount][hostAccountId].hostAccountAddress=address - // the interchain account address on the host chain - // must remain the same after the upgrade. - abortTransactionUnless(currentMetadata.Address === metadata.Address) +return address +} - // at the moment it is not supported to perform upgrades that - // change the connection ID of the controller or host chains. - // therefore these connection IDs much remain the same as before. - abortTransactionUnless(currentMetadata.ControllerConnectionId === metadata.ControllerConnectionId) - abortTransactionUnless(currentMetadata.HostConnectionId === metadata.HostConnectionId) - // the proposed connection hop must not change - abortTransactionUnless(currentMetadata.HostConnectionId === connectionHops[0]) +// Retrieves the interchain account from state. +function getInterchainAccountAddress(portId: string, channelId: string, icaOwnerAddress: string, hostAccountId:uint64) : string { - return counterpartyVersion, nil +return hostAccounts[portId][channelId][icaOwnerAddress][hostAccountId].hostAccountAddress } ``` +##### Helper Function + +The helper functions described herein must be implemented in a hosting interchain account module. + +###### **registerInterchainAccount** + +`registerInterchainAccount` may be called during the `OnReceive` callback when a `REGISTER_TX` tx type is relayed to the host chain and contains the parameter `usedIds`. + ```typescript -// Called on Controller Chain by Relayer -function onChanUpgradeAck( - portIdentifier: Identifier, - channelIdentifier: Identifier, - counterpartyVersion: string -): Error { - // final upgrade version proposed by counterparty - abortTransactionUnless(counterpartyVersion !== "") - metadata = UnmarshalJSON(counterpartyVersion) - - // retrieve the existing channel version. - // In ibc-go, for example, this is done using the GetAppVersion - // function of the ICS4Wrapper interface. - // See https://github.com/cosmos/ibc-go/blob/ac6300bd857cd2bd6915ae51e67c92848cbfb086/modules/core/05-port/types/module.go#L128-L132 - channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) - abortTransactionUnless(channel !== null) - currentMetadata = UnmarshalJSON(channel.version) +function registerInterchainAccount( + portId: string, + channelId: string, + counterpartyPortId: string, + counterpartyChannelId: string, + icaOwnerAccount: string, + usedIds: []uint64 + ) { - // validate metadata - abortTransactionUnless(metadata.Version === "ics27-1") - // all elements in encoding list and tx type list must be supported - abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) - abortTransactionUnless(IsSupportedTxType(metadata.TxType)) + // validate port format + abortTransactionUnless(portId=="ica") + // retrieve channel + channel = provableStore.get(channelPath(portId, channelId)) + // validate that the channel infos + //abortTransactionUnless(isActive(channel)) + abortTransactionUnless(channel.counterpartyPortId == counterpartyPortId) + abortTransactionUnless(channel.counterpartyChannelId == counterpartyChannelId) + + for seq in usedIds{ + setInterchainAccountAddress(portId,channelId,icaOwnerAddress,seq) + } + +} +``` - // the interchain account address on the host chain - // must remain the same after the upgrade. - abortTransactionUnless(currentMetadata.Address === metadata.Address) +##### **executeTx** - // at the moment it is not supported to perform upgrades that - // change the connection ID of the controller or host chains. - // therefore these connection IDs much remain the same as before. - abortTransactionUnless(currentMetadata.ControllerConnectionId === metadata.ControllerConnectionId) - abortTransactionUnless(currentMetadata.HostConnectionId === metadata.HostConnectionId) +Executes each message sent by the owner account on the controller chain. - return nil +```typescript +function executeTx(hostAccount: string, msg Any) : (resultString, error) { + + // Signature has already been validated in the sendTx + // Execute the msg for the given hostAccount + + // Review Syntax + return hostAccount.execute(msg), nil + // return result of transaction } ``` +##### **msgBlacklist** + +The `addMessageToBlacklist` can be called by the host chain to blacklist certain types of msgs. + ```typescript -// Called on Controller and Host Chains by Relayer -function onChanUpgradeOpen( - portIdentifier: Identifier, - channelIdentifier: Identifier) { - // no-op -} +function addMessageToBlacklist(portId: string, channelId: string, icaOwnerAddress: string, msgType: string): error { + + if(msgType==msgTransfer){ + return "MsgTransfer cannot be blacklisted" + } + + if(msgType.isIn(msgBlacklist[portId][channelId][icaOwnerAddress])==false){ + msgBlacklist[portId][channelId][icaOwnerAddress].add(msgType) + return nil + } else { + return "Message type already blacklisted" + } +} ``` -### Packet relay +##### Packet relay `onRecvPacket` is called by the routing module when a packet addressed to this module has been received. ```typescript // Called on Host Chain by Relayer function onRecvPacket(packet Packet) { - ack = NewResultAcknowledgement([]byte{byte(1)}) - - // only attempt the application logic if the packet data - // was successfully decoded - switch data.Type { - case types.EXECUTE_TX: - msgs, err = types.DeserializeTx(data.Data) - if err != nil { - return NewErrorAcknowledgement(err) - } + // The host chain can receive two interchain account packet types: REGISTER_TX and EXECUTE_TX + // Thus we must handle both cases in the onRecv + switch packet.data.icaType { + case types.REGISTER: - // ExecuteTx calls the AuthenticateTx function defined above - result, err = ExecuteTx(ctx, packet.SourcePort, packet.DestinationPort, packet.DestinationChannel, msgs) - if err != nil { - // NOTE: The error string placed in the acknowledgement must be consistent across all - // nodes in the network or there will be a fork in the state machine. - return NewErrorAcknowledgement(err) - } + (_,icaOwnerAccount,hostAccountIds,err) = types.Deserialize(packet.data) + + if err != nil { + return NewErrorAcknowledgement(err) + } - // return acknowledgement containing the transaction result after executing on host chain - return NewAcknowledgement(result) + // We want to return the addresses that will be generated in an array of string placed in the ack. + let newAccounts: []string = [] + + for seq in hostAccountIds { + if getInterchainAccountAddress(portId,channelId,icaOwnerAddress,hostAccountId)!=="") { + return NewErrorAcknowledgement("hostAccountId already used") + } + + registerInterchainAccount(Packet.DestinationPort,Packet.DestinationChannel,Packet.SourcePort,Packet.SourceChannel, icaOwnerAccount,seq) + // We want to return in the ack only the new generated accounts. So we retrieve them from the module state + newAccounts.push(hostAccounts[seq].hostAccountAddress) + } + + //icaPacketAcknowledgement = icaExecutePacketSuccess | icaRegistrationPacketSuccess | icaPacketError; + icaPacketAcknowledgement ack = icaRegisterPacketSuccess{newAccounts} + return ack + case types.EXECUTE: + + (_,icaOwnerAccount,hostAccountIds,msgs,memo,err) = types.Deserialize(packet.data) + + if err != nil { + return NewErrorAcknowledgement(err) + } + + // TODO Check Optimizations + let resultsData : [] bytes = [] + let executingAddress: string = "" + let hostAddressesSet: set(string) + + // Construct the set of addresses given the hostAccountIds + for seq in hostAccountIds{ + temp=getInterchainAccountAddress(portId,channelId,icaOwnerAddress,seq) + if temp=="" { + return NewErrorAcknowledgement("Requesting Tx For A Non Registered Account") + } + // If multplie msgs refer to the same host account and the address has already been stored in the hostAddressSet, we don't need to restore it. + if(!temp.isIn(hostAddressesSet)) { + // If not already stored, then store it. + hostAddressesSet.add(temp) + } + } + + for msg in msgs{ + // TODO Include check for blacklisted message. + if(msg.type.isIn(msgBlackList[portId][channelId][icaOwnerAddress])){ + return NewErrorAcknowledgement("The controller chain is trying to execute a message that has been blacklisted by the host chain.") + } + + executingAddress = msg.expectedSigner() + // Verify that the expectedSigner is in the set of host addresses constructed for this IBC tx. + // Here the idea is that we confirm that the expected signer is part of a set of the hostAccountsAddress set constructed with the hostAccountIds passed in by the icaOwnerAddress. Is this enough? + if(executingAddress.isIn(hostAddressesSet)==false){ + return NewErrorAcknowledgement("Expected Signer Mismatch") + } + + // executeTx executes each message individually + resultData, err = executeTx(executingAddress, msg) + if err != nil { + // In case any of the msg in the for loop fails, everything will be reverted by returning the error ack providing atomiticy between msgs of the same ica packet. + return NewErrorAcknowledgement(err) + } + // Only push result if no error is detected + resultsData.push(resultData) + } + + InterchainAccountPacketAcknowledgement ack = icaExecutePacketSuccess{resultData} + + return ack + } + default: return NewErrorAcknowledgement(ErrUnknownDataType) } -} ``` -`onAcknowledgePacket` is called by the routing module when a packet sent by this module has been acknowledged. +### Register & controlling flows -```typescript -// Called on Controller Chain by Relayer -function onAcknowledgePacket( - packet: Packet, - acknowledgement: bytes -) { - // call underlying app's OnAcknowledgementPacket callback - // see ICS-30 middleware for more information -} -``` +// TODO: Provide diagrams for each of the flows [DiagramBaseImage](https://excalidraw.com/#json=BfGp0ZbDAhO_LWiNmdGGn,Mgw4FrmorzuGjM0alzLMWw +) -```typescript -// Called on Controller Chain by Relayer -function onTimeoutPacket(packet: Packet) { - // call underlying app's OnTimeoutPacket callback - // see ICS-30 middleware for more information -} -``` +#### Registering Flow -Note that interchain accounts controller modules should not execute any logic upon packet receipt, i.e. the `OnRecvPacket` callback should not be called, and in case it is called, it should simply return an error acknowledgement: +##### Success Case -```typescript -// Called on Controller Chain by Relayer -function onRecvPacket(packet Packet) { - return NewErrorAcknowledgement(ErrInvalidChannelFlow) -} +Precondition: The user on the controller chain has created an account. This account will be used as the `icaOwnerAddress`. + +- 0.1 The user creates a `registerTx` Tx1. +- 0.2 The user sign Tx1 with the `icaOwnerAddress` +- 0.3 The user sends Tx1 to the controller state machine. + +- 1.1 The controller state machine passes the transaction to the proper icaTxHandler. +- 1.2 The `icaRegisterTxHandler` validates Tx1 and executes signature checks over `icaOwnerAddress` verifying it is the signer of Tx1. +- 1.3 The `icaRegisterTxHandler` calls `sendRegisterTx` + +- 2.1 The `sendRegisterTx` calls the `registerInterchainAccount` controller function. +- 2.2 The `registerInterchainAccount` computes the `usedHostAccountsIds` +- 2.3 The `sendRegisterTx` construct and sends the packet, via ICS-4 wrapper, with `icaRegisterPacketData` information (containing the `icaOwnerAddress` and the `usedHostAccountsIds`) + +- 3.1 The relayer relays the packet to the host state machine. + +- 4.1 The host state machine dispatches the packet to the proper module handler. +- 4.2 The `onRecvPacket` callback is activated on the host state machine and triggers the `registerInterchainAccount` function. +- 4.3 The `registerInterchainAccount` function of the host chain verifies that the `usedHostAccountsIds` have not already been used. +- 4.4 The `registerInterchainAccount` generates the addresses based on the passed-in parameters. +- 4.5 The addresses are stored in the host chain module state. +- 4.6 Upon completion, the `onRecvPacket` callback writes an acknowledgment containing the newly generated addresses. + +- 5.1 The relayer relays the acknowledgment packet to the controller state machine. + +- 6.1 The `onAcknowledgePacket` is activated on the controller state machine +- 6.2 The addresses contained in the acknowledgment are written into the controller chain module state. + +// Test mermaid diagram + +```mermaid +sequenceDiagram + participant User + participant Controller + participant Controller - icaRegisterTxHandler + participant Controller - SendRegisterTx + participant Controller - registerInterchainAccount + participant Controller - onAcknowledgePacket + participant Controller - ICS4 + participant Relayer + participant Host + participant Host - onReceive + participant Host - registerInterchainAccount + + + Note over User: Precondition: User has created an account icaOwnerAddress on the controller chain + + User->>User: 0.1 create registerTx Tx1 + User->>User: 0.2 sign Tx1 with icaOwnerAddress + User->>Controller: 0.3 send Tx1 to controller state machine + + Controller->>Controller - icaRegisterTxHandler: 1.1 pass Tx1 to proper icaTxHandler + Controller - icaRegisterTxHandler->>Controller - icaRegisterTxHandler: 1.2 validate Tx1 and check signature of icaOwnerAddress + Controller - icaRegisterTxHandler->>Controller - SendRegisterTx: 1.3 call sendRegisterTx + + Controller - SendRegisterTx ->>Controller - registerInterchainAccount: 2.1 call registerInterchainAccount + Controller - registerInterchainAccount->>Controller - registerInterchainAccount : 2.2 compute usedHostAccountsIds + Controller - SendRegisterTx->> Controller - ICS4: 2.3 construct packet and invoke ICS4 wrapper sendPacket + Controller - ICS4 ->> Controller - ICS4: 2.4 send packet with icaRegisterPacketData + + Relayer->>Host: 3.1 relay packet to host state machine + + Host->>Host - onReceive: 4.1 dispatch packet to proper module handler + Host - onReceive->>Host - onReceive: 4.2 verify usedHostAccountsIds + Host - onReceive->>Host - registerInterchainAccount : 4.3 calls registerInterchainAccount + Host - registerInterchainAccount->> Host - onReceive: 4.4 generate addresses based on parameters + Host - onReceive->>Host: 4.5 store addresses in module state + Host - onReceive->> Host - onReceive: 4.6 write acknowledgment with new addresses + + Relayer->>Controller: 5.1 relay acknowledgment packet to controller state machine + + + Controller->>Controller - onAcknowledgePacket: 6.1 activate onAcknowledgePacket + Controller - onAcknowledgePacket->>Controller: 6.2 write addresses into module state ``` -### Identifier formats +##### Error Case + +Precondition: The user on the controller chain has created an account. This account will be used as the `icaOwnerAddress`. + +- 0.1 The user creates a `registerTx` Tx1. +- 0.2 The user sign Tx1 with the `icaOwnerAddress` +- 0.3 The user sends Tx1 to the controller state machine. + +- 1.1 The controller state machine passes the transaction to the proper icaTxHandler. +- 1.2 The `icaRegisterTxHandler` validates Tx1 and executes signature checks over `icaOwnerAddress` verifying it is the signer of Tx1. +- 1.3 The `icaRegisterTxHandler` calls `sendRegisterTx` + +- 2.1 The `sendRegisterTx` calls the `registerInterchainAccount` controller function. +- 2.2 The `registerInterchainAccount` computes the `usedHostAccountsIds` +- 2.3 The `sendRegisterTx` construct and sends the packet, via ICS-4 wrapper, with `icaRegisterPacketData` information (containing the `icaOwnerAddress` and the `usedHostAccountsIds`) + +- 3.1 The relayer relays the packet to the host state machine. + +- 4.1 The host state machine dispatches the packet to the proper module handler. +- 4.2 The `onRecvPacket` callbacks are activated on the host state machine. +- 4.3 The `onRecvPacket` function triggers an error, thus it returns an error acknowledgment. + +- 5.1 The relayer relays the error acknowledgment packet to the controller state machine. + +- 6.1 The `onAcknowledgePacket` is activated on the controller state machine +- 6.2 The `usedHostAccountsIds` are recovered and stored in the `unusedHostAccountsIds` array. + +Note that `onTimeout` a similar logic is triggered with the `usedHostAccountsIds` that get recovered and stored in the `unusedHostAccountsIds` array. + +#### Controlling Flow + +##### Success Case + +Precondition: The user on the controller chain has registered an account on the host chain. The message that will be passed in must use an already registered account address. + +- 0.1 The user creates a `executeTx` Tx2. +- 0.2 The user signs Tx2 with the `icaOwnerAddress`. +- 0.3 The user sends Tx2 to the controller state machine. + +- 1.1 The controller state machine passes the transaction to the proper icaTxHandler. +- 1.2 The `icaExecuteTxHandler` validates Tx2 and executes signatures checks over the `icaOwnerAddress` verifying this is the signer of Tx2. +- 1.3 The `icaExecuteTxHandler` calls `sendExecuteTx`. + +- 2.1 The `sendExecuteTx` verifies that the `hostAccountIds` passed in are actually related to an already registered `hostAccountAddress` and that the messages array is not empty. +- 2.2 The `sendExecuteTx` constructs and sends the packet, via ICS-4 wrapper, with `icaExecutePacketData` information (containing the `icaOwnerAddress` and the `hostAccountsIds` and the `msgs`). + +- 3.1 The relayer relays the packet to the host state machine. + +- 4.1 The host state machine dispatches the packet to the proper module handler. +- 4.2 The `onRecvPacket` callback is activated on the host state machine. +- 4.3 During the `onRecvPacket` the addresses set is constructed given the `hostAccountIds` (retrieve the addresses from the module state given the `hostAccountId` key). +- 4.4 For every msg contained in the `msgs` array, the `onRecvPacket` verifies that the `msg.expectedSigner` is contained in constructed the addresses set. +- 4.5 The msg is executed and the return values are saved in the `resultData`. Note that here the msg should be passed to the right module which, considering that the msg is coming from an ica Tx, should skip the signature verification. // NOTE probably the application module should have a dedicated entrypoint for this case. +- 4.6 Once all the msgs are executed, the acknowledgment containing `resultData` is returned. -These are the default formats that the port identifiers on each side of an interchain accounts channel. The controller portID **must** include the owner address so that when a message is sent to the controller module, the sender of the message can be verified against the portID before sending the ICA packet. The controller chain is responsible for proper access control to ensure that the sender of the ICA message has successfully authenticated before the message reaches the controller module. +- 5.1 The relayer relays the acknowledgment packet to the controller state machine. -Controller Port Identifier: optional prefix `icacontroller-` + mandatory `{owner-account-address}` +- 6.1 The `onAcknowledgePacket` is activated on the controller state machine triggering a noOp. -Host Port Identifier: `icahost` +##### Error Case -The `icacontroller-` prefix on the controller port identifier is optional and host chains **must** not enforce that the counterparty port identifier includes it. Controller chains may decide to include it and validate that it is present in their own port identifier. +Precondition: The user on the controller chain has registered an account on the host chain. The message that will be passed in must use an already registered account address. + +- 0.1 The user creates an `executeTx` Tx2. +- 0.2 The user signs Tx2 with the `icaOwnerAddress`. +- 0.3 The user sends Tx2 to the controller state machine. + +- 1.1 The controller state machine passes the transaction to the proper icaTxHandler. +- 1.2 The `icaExecuteTxHandler` validates Tx2 and executes signatures checks over the `icaOwnerAddress` verifying this is the signer of Tx2. +- 1.3 The `icaExecuteTxHandler` calls `sendExecuteTx`. + +- 2.1 The `sendExecuteTx` verifies that the `hostAccountIds` passed in are actually related to an already registered `hostAccountAddress` and that the messages array is not empty. +- 2.2 The `sendRegisterTx` construct and sends the packet, via ICS-4 wrapper, with `icaExecutePacketData` information (containing the `icaOwnerAddress` and the `hostAccountsIds` and the `msgs`). + +- 3.1 The relayer relays the packet to the host state machine. + +- 4.1 The host state machine dispatches the packet to the proper module handler. +- 4.2 The `onRecvPacket` callbacks are activated on the host state machine. +- 4.3 The `onRecvPacket` function triggers an error, thus it returns an error acknowledgment. + +- 5.1 The relayer relays the error acknowledgment packet to the controller state machine. + +- 6.1 The `onAcknowledgePacket` is activated on the controller state machine triggering a noOp (nothing to revert on the controller chain). + +## Considerations + +### Message Execution Ordering + +Problem: +Given chain A and chain B, if chain A sends two IBC packets, each one containing an `EXECUTE_TX` message, the order of execution of the packets, and so of the messages, is not guaranteed because it depends on the relayer delivery order of the packets themself. + +Solution: +The user who needs a certain order of execution for its messages Must place them in the same IBC ica packet. When the messages are placed in the same IBC packet, we can guarantee the atomicity and the order of execution. Indeed if any of the messages fails, everything will be reverted by writing an error acknowledgment, and, additionally, the messages that are passed in an interchain account `EXECUTE_TX` will be executed by the host chain following a FIFO mechanism. + +### Account balances post execution on the host chain + +In the case the controller chain wants to know the host account balance after certain msgs are executed, it should include a cross-chain-query message at the bottom of the msg list. Then the result can be retrieved from the event emitted in the acknowledgment. + +### Interchain Account Recovery + +Since we are allowing only unordered channels and disallowing channel closure procedures, we don't need a procedure for recovering. The channel cannot be closed or get stuck. + +### MsgBlacklist + +Problem: +What happens if the controller chain registers and funds a hostAccount and then the host chain blacklist the transfers? Are funds lost? + +Solution: +Don't allow the host chain to blacklist msgTransfer. This is the only msg that cannot be blacklisted. So that the controller chain can always have the opportunity to recover funds. ## Example Implementations -- Implementation of ICS 27 in Go can be found in [ibc-go repository](https://github.com/cosmos/ibc-go). +- Implementation of ICS 27 version 1 in Go can be found in the [ibc-go repository](https://github.com/cosmos/ibc-go). + +- Implementation of ICS 27 version 2 in Go COMING SOON. ## Future Improvements -A future version of interchain accounts may be greatly simplified by the introduction of an IBC channel type that is ORDERED but does not close the channel on timeouts, and instead proceeds to accept and receive the next packet. If such a channel type is made available by core IBC, Interchain accounts could require the use of this channel type and remove all logic and state pertaining to "active channels". The metadata format can also be simplified to remove any reference to the underlying connection identifiers. +// TODO + +A future version of interchain accounts may be greatly simplified by the introduction of an IBC channel type that is ORDERED but does not close the channel on timeouts and instead proceeds to accept and receive the next packet. If such a channel type is made available by core IBC, Interchain accounts could require the use of this channel type and remove all logic and state pertaining to "active channels". The metadata format can also be simplified to remove any reference to the underlying connection identifiers. The "active channel" setting and unsetting is currently necessary to allow interchain account owners to create a new channel in case the current active channel closes during channel timeout. The connection identifiers are part of the metadata to ensure that any new channel that gets opened are established on top of the original connection. All of this logic becomes unnecessary once the channel is ordered **and** unclosable, which can only be achieved by the introduction of a new channel type to core IBC. @@ -815,6 +1118,8 @@ November 11, 2021 - Update with latest changes from implementation December 14, 2021 - Revisions to spec based on audits and maintainer reviews August 1, 2023 - Implemented channel upgrades callbacks + +July 4, 2024 - [ICS-27 version 2 draft suggested](https://github.com/cosmos/ibc/pull/1122) ## Copyright diff --git a/spec/app/ics-027-interchain-accounts/v1/README.md b/spec/app/ics-027-interchain-accounts/v1/README.md new file mode 100644 index 000000000..28cd292f1 --- /dev/null +++ b/spec/app/ics-027-interchain-accounts/v1/README.md @@ -0,0 +1,822 @@ +--- +ics: 27 +title: Interchain Accounts +version: 1 +stage: Draft +category: IBC/APP +requires: 25, 26 +kind: instantiation +version compatibility: +author: Tony Yun , Dogemos , Sean King +created: 2019-08-01 +modified: 2020-07-14 +--- + +## Synopsis + +This standard document specifies packet data structure, state machine handling logic, and encoding details for the account management system over an IBC channel between separate chains. + +### Motivation + +ICS-27 Interchain Accounts outlines a cross-chain account management protocol built upon IBC. ICS-27 enabled chains can programmatically create accounts on other ICS-27 enabled chains & control these accounts via IBC transactions (instead of signing with a private key). Interchain accounts retain all of the capabilities of a normal account (i.e. stake, send, vote) but instead are managed by a separate chain via IBC in a way such that the owner account on the controller chain retains full control over any interchain account(s) it registers on host chain(s). + +### Definitions + +- `Host Chain`: The chain where the interchain account is registered. The host chain listens for IBC packets from a controller chain which contain instructions (e.g. cosmos SDK messages) that the interchain account will execute. +- `Controller Chain`: The chain registering and controlling an account on a host chain. The controller chain sends IBC packets to the host chain to control the account. +- `Interchain Account`: An account on a host chain. An interchain account has all the capabilities of a normal account. However, rather than signing transactions with a private key, a controller chain will send IBC packets to the host chain which signals what transactions the interchain account must execute. +- `Interchain Account Owner`: An account on the controller chain. Every interchain account on a host chain has a respective owner account on the controller chain. + +The IBC handler interface & IBC relayer module interface are as defined in [ICS-25](../../../core/ics-025-handler-interface) and [ICS-26](../../../core/ics-026-routing-module), respectively. + +### Desired properties + +- Permissionless: An interchain account may be created by any actor without the approval of a third party (e.g. chain governance). Note: Individual implementations may implement their own permissioning scheme, however the protocol must not require permissioning from a trusted party to be secure. +- Fault isolation: A controller chain must not be able to control accounts registered by other controller chains. For example, in the case of a fork attack on a controller chain, only the interchain accounts registered by the forked chain will be vulnerable. +- The ordering of transactions sent to an interchain account on a host chain must be maintained. Transactions must be executed by an interchain account in the order in which they are sent by the controller chain. +- If a channel closes, the controller chain must be able to regain access to registered interchain accounts by simply opening a new channel. +- Each interchain account is owned by a single account on the controller chain. Only the owner account on the controller chain is authorized to control the interchain account. The controller chain is responsible for enforcing this logic. +- The controller chain must store the account address of any owned interchain accounts registered on host chains. +- A host chain must have the ability to limit interchain account functionality on its chain as necessary (e.g. a host chain can decide that interchain accounts registered on the host chain cannot take part in staking). + +## Technical specification + +### General design + +A chain can utilize one or both parts of the interchain accounts protocol (*controlling* and *hosting*). A controller chain that registers accounts on other host chains (that support interchain accounts) does not necessarily have to allow other controller chains to register accounts on its chain, and vice versa. + +This specification defines the general way to register an interchain account and send tx bytes to be executed on behalf of the owner account. The host chain is responsible for deserializing and executing the tx bytes and the controller chain must know how the host chain will handle the tx bytes in advance of sending a packet, thus this must be negotiated during channel creation. + +### Controller chain contract + +#### **RegisterInterchainAccount** + +`RegisterInterchainAccount` is the entry point to registering an interchain account. +It generates a new controller portID using the owner account address. +It will bind to the controller portID and +call 04-channel `ChanOpenInit`. An error is returned if the controller portID is already in use. +A `ChannelOpenInit` event is emitted which can be picked up by an offchain process such as a relayer. +The account will be registered during the `OnChanOpenTry` step on the host chain. +This function must be called after an `OPEN` connection is already established with the given connection identifier. +The caller must provide the complete channel version. This MUST include the ICA version with complete metadata and it MAY include +versions of other middleware that is wrapping ICA on both sides of the channel. Note this will require contextual information +on what middleware is enabled on either end of the channel. Thus it is recommended that an ICA-auth application construct the ICA +version automatically and allow for users to optionally enable additional middleware versioning. + +```typescript +function RegisterInterchainAccount(connectionId: Identifier, owner: string, version: string) returns (error) { +} +``` + +#### **SendTx** + +`SendTx` is used to send an IBC packet containing instructions (messages) to an interchain account on a host chain for a given interchain account owner. + +```typescript +function SendTx( + capability: CapabilityKey, + connectionId: Identifier, + portId: Identifier, + icaPacketData: InterchainAccountPacketData, + timeoutTimestamp uint64 +): uint64 { + // check if there is a currently active channel for + // this portId and connectionId, which also implies an + // interchain account has been registered using + // this portId and connectionId + activeChannelID, found = GetActiveChannelID(portId, connectionId) + abortTransactionUnless(found) + + // validate timeoutTimestamp + abortTransactionUnless(timeoutTimestamp <= currentTimestamp()) + + // validate icaPacketData + abortTransactionUnless(icaPacketData.type == EXECUTE_TX) + abortTransactionUnless(icaPacketData.data != nil) + + // send icaPacketData to the host chain on the active channel + sequence = handler.sendPacket( + capability, + portId, // source port ID + activeChannelID, // source channel ID + 0, + timeoutTimestamp, + protobuf.marshal(icaPacketData) // protobuf-marshalled bytes of packet data + ) + + return sequence +} +``` + +### Host chain contract + +#### **RegisterInterchainAccount** + +`RegisterInterchainAccount` is called on the `OnChanOpenTry` step during the channel creation handshake. + +```typescript +function RegisterInterchainAccount(counterpartyPortId: Identifier, connectionID: Identifier) returns (nil) { + // checks to make sure the account has not already been registered + // creates a new address on chain deterministically given counterpartyPortId and underlying connectionID + // calls SetInterchainAccountAddress() +} +``` + +#### **AuthenticateTx** + +`AuthenticateTx` is called before `ExecuteTx`. +`AuthenticateTx` checks that the signer of a particular message is the interchain account associated with the counterparty portID of the channel that the IBC packet was sent on. + +```typescript +function AuthenticateTx(msgs []Any, connectionId string, portId string) returns (error) { + // GetInterchainAccountAddress(portId, connectionId) + // if interchainAccountAddress != msgSigner return error +} +``` + +#### **ExecuteTx** + +Executes each message sent by the owner account on the controller chain. + +```typescript +function ExecuteTx(sourcePort: Identifier, channel Channel, msgs []Any) returns (resultString, error) { + // validate each message + // retrieve the interchain account for the given channel by passing in source port and channel's connectionID + // verify that interchain account is authorized signer of each message + // execute each message + // return result of transaction +} +``` + +### Utility functions + +```typescript +// Sets the active channel for a given portID and connectionID. +function SetActiveChannelID(portId: Identifier, connectionId: Identifier, channelId: Identifier) returns (error){ +} + +// Returns the ID of the active channel for a given portID and connectionID, if present. +function GetActiveChannelID(portId: Identifier, connectionId: Identifier) returns (Identifier, boolean){ +} + +// Stores the address of the interchain account in state. +function SetInterchainAccountAddress(portId: Identifier, connectionId: Identifier, address: string) returns (string) { +} + +// Retrieves the interchain account from state. +function GetInterchainAccountAddress(portId: Identifier, connectionId: Identifier) returns (string, bool){ +} +``` + +### Register & controlling flows + +#### Register account flow + +To register an interchain account we require an off-chain process (relayer) to listen for `ChannelOpenInit` events with the capability to finish a channel creation handshake on a given connection. + +1. The controller chain binds a new IBC port with the controller portID for a given *interchain account owner address*. + +This port will be used to create channels between the controller & host chain for a specific owner/interchain account pair. Only the account with `{owner-account-address}` matching the bound port will be authorized to send IBC packets over channels created with the controller portID. It is up to each controller chain to enforce this port registration and access on the controller side. + +2. The controller chain emits an event signaling to open a new channel on this port given a connection. +3. A relayer listening for `ChannelOpenInit` events will continue the channel creation handshake. +4. During the `OnChanOpenTry` callback on the host chain an interchain account will be registered and a mapping of the interchain account address to the owner account address will be stored in state (this is used for authenticating transactions on the host chain at execution time). +5. During the `OnChanOpenAck` callback on the controller chain a record of the interchain account address registered on the host chain during `OnChanOpenTry` is set in state with a mapping from (controller portID, controller connectionID) -> interchain account address. See [metadata negotiation](#metadata-negotiation) section below for how to implement this. +6. During the `OnChanOpenAck` & `OnChanOpenConfirm` callbacks on the controller & host chains respectively, the [active-channel](#active-channels) for this interchain account/owner pair, is set in state. + +#### Active channels + +The controller and host chain must keep track of an `active-channel` for each registered interchain account. The `active-channel` is set during the channel creation handshake process. This is a safety mechanism that allows a controller chain to regain access to an interchain account on a host chain in case of a channel closing. + +An example of an active channel on the controller chain can look like this: + +```typescript +{ + // Controller Chain + SourcePortId: `icacontroller-`, + SourceChannelId: ``, + // Host Chain + CounterpartyPortId: `icahost`, + CounterpartyChannelId: ``, +} +``` + +In the event of a channel closing, the active channel may be replaced by starting a new channel handshake with the same port identifiers on the same underlying connection of the original active channel. ICS-27 channels can only be closed in the event of a timeout (if the implementation uses ordered channels) or in the unlikely event of a light client attack. Controller chains must retain the ability to open new ICS-27 channels and reset the active channel for a particular portID (containing `{owner-account-address}`) and connectionID pair. + +The controller and host chains must verify that any new channel maintains the same metadata as the previous active channel to ensure that the parameters of the interchain account remain the same even after replacing the active channel. The `Address` of the metadata should not be verified since it is expected to be empty at the INIT stage, and the host chain will regenerate the exact same address on TRY, because it is expected to generate the interchain account address deterministically from the controller portID and connectionID (both of which must remain the same). + +#### **Metadata negotiation** + +ICS-27 takes advantage of [ICS-04 channel version negotiation](../../../core/ics-004-channel-and-packet-semantics/README.md#versioning) to negotiate metadata and channel parameters during the channel handshake. The metadata will contain the encoding format along with the transaction type so that the counterparties can agree on the structure and encoding of the interchain transactions. The metadata sent from the host chain on the TRY step will also contain the interchain account address, so that it can be relayed to the controller chain. At the end of the channel handshake, both the controller and host chains will store a mapping of (controller chain portID, controller/host connectionID) to the newly registered interchain account address ([account registration flow](#register-account-flow)). + +ICS-04 allows for each channel version negotiation to be application-specific. In the case of interchain accounts, the channel version will be a string of a JSON struct containing all the relevant metadata intended to be relayed to the counterparty during the channel handshake step ([see summary below](#metadata-negotiation-summary)). + +Combined with the one channel per interchain account approach, this method of metadata negotiation allows us to pass the address of the interchain account back to the controller chain and create a mapping from (controller portID, controller connection ID) -> interchain account address during the `OnChanOpenAck` callback. As outlined in the [controlling flow](#controlling-flow), a controller chain will need to know the address of a registered interchain account in order to send transactions to the account on the host chain. + +#### **Metadata negotiation summary** + +`interchain-account-address` is the address of the interchain account registered on the host chain by the controller chain. + +- **INIT** + +Initiator: Controller + +Datagram: ChanOpenInit + +Chain Acted Upon: Controller + +Version: + +```json +{ + "Version": "ics27-1", + "ControllerConnectionId": "self_connection_id", + "HostConnectionId": "counterparty_connection_id", + "Address": "", + "Encoding": "requested_encoding_type", + "TxType": "requested_tx_type", +} +``` + +Comments: The address is left empty since this will be generated and relayed back by the host chain. The connection identifiers must be included to ensure that if a new channel needs to be opened (in case active channel times out), then we can ensure that the new channel is opened on the same connection. This will ensure that the interchain account is always connected to the same counterparty chain. + +- **TRY** + +Initiator: Relayer + +Datagram: ChanOpenTry + +Chain Acted Upon: Host + +Version: + +```json +{ + "Version": "ics27-1", + "ControllerConnectionId": "counterparty_connection_id", + "HostConnectionId": "self_connection_id", + "Address": "interchain_account_address", + "Encoding": "negotiated_encoding_type", + "TxType": "negotiated_tx_type", +} +``` + +Comments: The ICS-27 application on the host chain is responsible for returning this version given the counterparty version set by the controller chain in INIT. The host chain must agree with the single encoding type and a single tx type that is requested by the controller chain (ie. included in counterparty version). If the requested encoding or tx type is not supported, then the host chain must return an error and abort the handshake. +The host chain must also generate the interchain account address and populate the address field in the version with the interchain account address string. + +- **ACK** + +Initiator: Relayer + +Datagram: ChanOpenAck + +Chain Acted Upon: Controller + +CounterpartyVersion: + +```json +{ + "Version": "ics27-1", + "ControllerConnectionId": "self_connection_id", + "HostConnectionId": "counterparty_connection_id", + "Address": "interchain_account_address", + "Encoding": "negotiated_encoding_type", + "TxType": "negotiated_tx_type", +} +``` + +Comments: On the ChanOpenAck step, the ICS27 application on the controller chain must verify the version string chosen by the host chain on ChanOpenTry. The controller chain must verify that it can support the negotiated encoding and tx type selected by the host chain. If either is unsupported, then it must return an error and abort the handshake. +If both are supported, then the controller chain must store a mapping from the channel's portID to the provided interchain account address and return successfully. + +#### Controlling flow + +Once an interchain account is registered on the host chain a controller chain can begin sending instructions (messages) to the host chain to control the account. + +1. The controller chain calls `SendTx` and passes message(s) that will be executed on the host side by the associated interchain account (determined by the controller side port identifier) + +Cosmos SDK pseudo-code example: + +```golang +// connectionId is the identifier for the controller connection +interchainAccountAddress := GetInterchainAccountAddress(portId, connectionId) +msg := &banktypes.MsgSend{FromAddress: interchainAccountAddress, ToAddress: ToAddress, Amount: amount} +icaPacketData = InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: serialize(msg), + Memo: "memo", +} + +// Sends the message to the host chain, where it will eventually be executed +SendTx(ownerAddress, connectionId, portID, data, timeout) +``` + +2. The host chain upon receiving the IBC packet will call `DeserializeTx`. + +3. The host chain will then call `AuthenticateTx` and `ExecuteTx` for each message and return an acknowledgment containing a success or error. + +Messages are authenticated on the host chain by taking the controller side port identifier and calling `GetInterchainAccountAddress(controllerPortId, hostConnectionId)` to get the expected interchain account address for the current controller port and connection identifier. If the signer of this message does not match the expected account address then authentication will fail. + +### Packet Data + +`InterchainAccountPacketData` contains an array of messages that an interchain account can execute and a memo string that is sent to the host chain as well as the packet `type`. ICS-27 version 1 has only one type `EXECUTE_TX`. + +```proto +message InterchainAccountPacketData { + enum type + bytes data = 1; + string memo = 2; +} +``` + +The acknowledgment packet structure is defined as in [ics4](https://github.com/cosmos/ibc-go/blob/main/proto/ibc/core/channel/v1/channel.proto#L135-L148). If an error occurs on the host chain the acknowledgment contains the error message. + +```proto +message Acknowledgement { + // response contains either a result or an error and must be non-empty + oneof response { + bytes result = 21; + string error = 22; + } +} +``` + +### Custom logic + +ICS-27 relies on [ICS-30 middleware architecture](../../ics-030-middleware) to provide the option for application developers to apply custom logic on the success or fail of ICS-27 packets. + +Controller chains will wrap `OnAcknowledgementPacket` & `OnTimeoutPacket` to handle the success or fail cases for ICS-27 packets. + +### Port & channel setup + +The interchain account module on a host chain must always bind to a port with the id `icahost`. Controller chains will bind to ports dynamically, as specified in the identifier format [section](#identifier-formats). + +The example below assumes a module is implementing the entire `InterchainAccountModule` interface. The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialized) to bind to the appropriate port. + +```typescript +function setup() { + capability = routingModule.bindPort("icahost", ModuleCallbacks{ + onChanOpenInit, + onChanOpenTry, + onChanOpenAck, + onChanOpenConfirm, + onChanCloseInit, + onChanCloseConfirm, + onChanUpgradeInit, // read-only + onChanUpgradeTry, // read-only + onChanUpgradeAck, // read-only + onChanUpgradeOpen, + onRecvPacket, + onTimeoutPacket, + onAcknowledgePacket, + onTimeoutPacketClose + }) + claimCapability("port", capability) +} +``` + +Once the `setup` function has been called, channels can be created via the IBC routing module. + +### Channel lifecycle management + +An interchain account module will accept new channels from any module on another machine, if and only if the channel initialization step is being invoked from the controller chain. + +```typescript +// Called on Controller Chain by InitInterchainAccount +function onChanOpenInit( + order: ChannelOrder, + connectionHops: [Identifier], + portIdentifier: Identifier, + channelIdentifier: Identifier, + counterpartyPortIdentifier: Identifier, + counterpartyChannelIdentifier: Identifier, + version: string +): (version: string, err: Error) { + // validate port format + abortTransactionUnless(validateControllerPortParams(portIdentifier)) + // only allow channels to be created on the "icahost" port on the counterparty chain + abortTransactionUnless(counterpartyPortIdentifier === "icahost") + + // retrieve channel and connection to access connection ID and counterparty connection ID + channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) + connectionId = channel.connectionHops[0] + connection = provableStore.get(connectionPath(connectionId)) + + if version != "" { + // validate metadata + metadata = UnmarshalJSON(version) + abortTransactionUnless(metadata.Version === "ics27-1") + // all elements in encoding list and tx type list must be supported + abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) + abortTransactionUnless(IsSupportedTxType(metadata.TxType)) + abortTransactionUnless(metadata.ControllerConnectionId === connectionId) + abortTransactionUnless(metadata.HostConnectionId === connection.counterpartyConnectionIdentifier) + } else { + // construct default metadata + metadata = { + Version: "ics27-1", + ControllerConnectionId: connectionId, + HostConnectionId: counterpartyConnectionId, + // implementation may choose a default encoding and TxType + // e.g. DefaultEncoding=protobuf, DefaultTxType=sdk.MultiMsg + Encoding: DefaultEncoding, + TxType: DefaultTxType, + } + version = marshalJSON(metadata) + } + + // only open the channel if: + // - there is no active channel already set (with status OPEN) + // OR + // - there is already an active channel (with status CLOSED) AND + // the metadata matches exactly the existing metadata in the + // version string of the active channel AND the ordering of the + // new channel matches the ordering of the active channel. + activeChannelId, activeChannelFound = GetActiveChannelID(portId, connectionId) + if activeChannelFound { + activeChannel = provableStore.get(channelPath(portId, activeChannelId)) + abortTransactionUnless(channel !== null) + abortTransactionUnless(activeChannel.state === CLOSED) + previousOrder = activeChannel.order + abortTransactionUnless(previousOrder === order) + previousMetadata = UnmarshalJSON(activeChannel.version) + abortTransactionUnless(previousMetadata === metadata) + } + + return version, nil +} +``` + +```typescript +// Called on Host Chain by Relayer +function onChanOpenTry( + order: ChannelOrder, + connectionHops: [Identifier], + portIdentifier: Identifier, + channelIdentifier: Identifier, + counterpartyPortIdentifier: Identifier, + counterpartyChannelIdentifier: Identifier, + counterpartyVersion: string +): (version: string, err: Error) { + // validate port ID + abortTransactionUnless(portIdentifier === "icahost") + + // retrieve channel and connection to access connection ID and counterparty connection ID + channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) + connectionId = channel.connectionHops[0] + connection = provableStore.get(connectionPath(connectionId)) + + // create the interchain account with the counterpartyPortIdentifier + // and the underlying connectionID on the host chain. + address = RegisterInterchainAccount(counterpartyPortIdentifier, connectionId) + + // state change to keep track of successfully registered interchain account + SetInterchainAccountAddress(counterpartyPortIdentifier, connectionId, address) + + cpMetadata = UnmarshalJSON(counterpartyVersion) + // it's not mandatory for the controller to fill in the host connection ID, since + // it could not be possible for it to know it. ibc-go's implementation of the + // controller does fill it in, but an CosmWasm controller implementation would + // not be able. For that reason, the host fills in here its own connection ID. + cpMetadata.HostConnectionId = connectionId + + abortTransactionUnless(cpMetadata.Version === "ics27-1") + // If encoding or txType requested by initializing chain is not supported by host chain then + // fail handshake and abort transaction + abortTransactionUnless(IsSupportedEncoding(cpMetadata.Encoding)) + abortTransactionUnless(IsSupportedTxType(cpMetadata.TxType)) + abortTransactionUnless(cpMetadata.ControllerConnectionId === connection.counterpartyConnectionIdentifier) + abortTransactionUnless(cpMetadata.HostConnectionId === connectionId) + + metadata = { + "Version": "ics27-1", + "ControllerConnectionId": cpMetadata.ControllerConnectionId, + "HostConnectionId": cpMetadata.HostConnectionId, + "Address": address, + "Encoding": cpMetadata.Encoding, + "TxType": cpMetadata.TxType, + } + + return string(MarshalJSON(metadata)), nil +} +``` + +```typescript +// Called on Controller Chain by Relayer +function onChanOpenAck( + portIdentifier: Identifier, + channelIdentifier: Identifier, + counterpartyChannelIdentifier, + counterpartyVersion: string +) { + // retrieve channel and connection to access connection ID and counterparty connection ID + channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) + connectionId = channel.connectionHops[0] + connection = provableStore.get(connectionPath(connectionId)) + + // validate counterparty metadata decided by host chain + metadata = UnmarshalJSON(version) + abortTransactionUnless(metadata.Version === "ics27-1") + abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) + abortTransactionUnless(IsSupportedTxType(metadata.TxType)) + abortTransactionUnless(metadata.ControllerConnectionId === connectionId) + abortTransactionUnless(metadata.HostConnectionId === connection.counterpartyConnectionIdentifier) + + // state change to keep track of successfully registered interchain account + SetInterchainAccountAddress(portID, metadata.ControllerConnectionId, metadata.Address) + // set the active channel for this owner/interchain account pair + SetActiveChannelID(portIdentifier, metadata.ControllerConnectionId, channelIdentifier) +} +``` + +```typescript +// Called on Host Chain by Relayer +function onChanOpenConfirm( + portIdentifier: Identifier, + channelIdentifier: Identifier +) { + channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) + abortTransactionUnless(channel !== null) + + // set the active channel for this owner/interchain account pair + SetActiveChannelID(channel.counterpartyPortIdentifier, channel.connectionHops[0], channelIdentifier) +} +``` + +```typescript +// The controller portID must have the format: `icacontroller-{ownerAddress}` +function validateControllerPortParams(portIdentifier: Identifier) { + split(portIdentifier, "-") + abortTransactionUnless(portIdentifier[0] === "icacontroller") + abortTransactionUnless(IsValidAddress(portIdentifier[1])) +} +``` + +### Closing handshake + +```typescript +function onChanCloseInit( + portIdentifier: Identifier, + channelIdentifier: Identifier) { + // disallow user-initiated channel closing for interchain account channels + abortTransactionUnless(FALSE) +} +``` + +```typescript +function onChanCloseConfirm( + portIdentifier: Identifier, + channelIdentifier: Identifier) { +} +``` + +### Upgrade handshake + +```typescript +// Called on Controller Chain by Authority +function onChanUpgradeInit( + portIdentifier: Identifier, + channelIdentifier: Identifier, + order: ChannelOrder, + connectionHops: [Identifier], + upgradeSequence: uint64, + version: string +): (version: string, err: Error) { + // new version proposed in the upgrade + abortTransactionUnless(version !== "") + metadata = UnmarshalJSON(version) + + // retrieve the existing channel version. + // In ibc-go, for example, this is done using the GetAppVersion + // function of the ICS4Wrapper interface. + // See https://github.com/cosmos/ibc-go/blob/ac6300bd857cd2bd6915ae51e67c92848cbfb086/modules/core/05-port/types/module.go#L128-L132 + channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) + abortTransactionUnless(channel !== null) + currentMetadata = UnmarshalJSON(channel.version) + + // validate metadata + abortTransactionUnless(metadata.Version === "ics27-1") + // all elements in encoding list and tx type list must be supported + abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) + abortTransactionUnless(IsSupportedTxType(metadata.TxType)) + + // the interchain account address on the host chain + // must remain the same after the upgrade. + abortTransactionUnless(currentMetadata.Address === metadata.Address) + + // at the moment it is not supported to perform upgrades that + // change the connection ID of the controller or host chains. + // therefore these connection IDs much remain the same as before. + abortTransactionUnless(currentMetadata.ControllerConnectionId === metadata.ControllerConnectionId) + abortTransactionUnless(currentMetadata.HostConnectionId === metadata.HostConnectionId) + // the proposed connection hop must not change + abortTransactionUnless(currentMetadata.ControllerConnectionId === connectionHops[0]) + + return version, nil +} +``` + +```typescript +// Called on Host Chain by Relayer +function onChanUpgradeTry( + portIdentifier: Identifier, + channelIdentifier: Identifier, + order: ChannelOrder, + connectionHops: [Identifier], + upgradeSequence: uint64, + counterpartyPortIdentifier: Identifier, + counterpartyChannelIdentifier: Identifier, + counterpartyVersion: string +): (version: string, err: Error) { + // validate port ID + abortTransactionUnless(portIdentifier === "icahost") + + // upgrade version proposed by counterparty + abortTransactionUnless(counterpartyVersion !== "") + metadata = UnmarshalJSON(counterpartyVersion) + + // retrieve the existing channel version. + // In ibc-go, for example, this is done using the GetAppVersion + // function of the ICS4Wrapper interface. + // See https://github.com/cosmos/ibc-go/blob/ac6300bd857cd2bd6915ae51e67c92848cbfb086/modules/core/05-port/types/module.go#L128-L132 + channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) + abortTransactionUnless(channel !== null) + currentMetadata = UnmarshalJSON(channel.version) + + // validate metadata + abortTransactionUnless(metadata.Version === "ics27-1") + // all elements in encoding list and tx type list must be supported + abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) + abortTransactionUnless(IsSupportedTxType(metadata.TxType)) + + // the interchain account address on the host chain + // must remain the same after the upgrade. + abortTransactionUnless(currentMetadata.Address === metadata.Address) + + // at the moment it is not supported to perform upgrades that + // change the connection ID of the controller or host chains. + // therefore these connection IDs much remain the same as before. + abortTransactionUnless(currentMetadata.ControllerConnectionId === metadata.ControllerConnectionId) + abortTransactionUnless(currentMetadata.HostConnectionId === metadata.HostConnectionId) + // the proposed connection hop must not change + abortTransactionUnless(currentMetadata.HostConnectionId === connectionHops[0]) + + return counterpartyVersion, nil +} +``` + +```typescript +// Called on Controller Chain by Relayer +function onChanUpgradeAck( + portIdentifier: Identifier, + channelIdentifier: Identifier, + counterpartyVersion: string +): Error { + // final upgrade version proposed by counterparty + abortTransactionUnless(counterpartyVersion !== "") + metadata = UnmarshalJSON(counterpartyVersion) + + // retrieve the existing channel version. + // In ibc-go, for example, this is done using the GetAppVersion + // function of the ICS4Wrapper interface. + // See https://github.com/cosmos/ibc-go/blob/ac6300bd857cd2bd6915ae51e67c92848cbfb086/modules/core/05-port/types/module.go#L128-L132 + channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) + abortTransactionUnless(channel !== null) + currentMetadata = UnmarshalJSON(channel.version) + + // validate metadata + abortTransactionUnless(metadata.Version === "ics27-1") + // all elements in encoding list and tx type list must be supported + abortTransactionUnless(IsSupportedEncoding(metadata.Encoding)) + abortTransactionUnless(IsSupportedTxType(metadata.TxType)) + + // the interchain account address on the host chain + // must remain the same after the upgrade. + abortTransactionUnless(currentMetadata.Address === metadata.Address) + + // at the moment it is not supported to perform upgrades that + // change the connection ID of the controller or host chains. + // therefore these connection IDs much remain the same as before. + abortTransactionUnless(currentMetadata.ControllerConnectionId === metadata.ControllerConnectionId) + abortTransactionUnless(currentMetadata.HostConnectionId === metadata.HostConnectionId) + + return nil +} +``` + +```typescript +// Called on Controller and Host Chains by Relayer +function onChanUpgradeOpen( + portIdentifier: Identifier, + channelIdentifier: Identifier) { + // no-op +} +``` + +### Packet relay + +`onRecvPacket` is called by the routing module when a packet addressed to this module has been received. + +```typescript +// Called on Host Chain by Relayer +function onRecvPacket(packet Packet) { + ack = NewResultAcknowledgement([]byte{byte(1)}) + + // only attempt the application logic if the packet data + // was successfully decoded + switch data.Type { + case types.EXECUTE_TX: + msgs, err = types.DeserializeTx(data.Data) + if err != nil { + return NewErrorAcknowledgement(err) + } + + // ExecuteTx calls the AuthenticateTx function defined above + result, err = ExecuteTx(ctx, packet.SourcePort, packet.DestinationPort, packet.DestinationChannel, msgs) + if err != nil { + // NOTE: The error string placed in the acknowledgement must be consistent across all + // nodes in the network or there will be a fork in the state machine. + return NewErrorAcknowledgement(err) + } + + // return acknowledgement containing the transaction result after executing on host chain + return NewAcknowledgement(result) + + default: + return NewErrorAcknowledgement(ErrUnknownDataType) + } +} +``` + +`onAcknowledgePacket` is called by the routing module when a packet sent by this module has been acknowledged. + +```typescript +// Called on Controller Chain by Relayer +function onAcknowledgePacket( + packet: Packet, + acknowledgement: bytes +) { + // call underlying app's OnAcknowledgementPacket callback + // see ICS-30 middleware for more information +} +``` + +```typescript +// Called on Controller Chain by Relayer +function onTimeoutPacket(packet: Packet) { + // call underlying app's OnTimeoutPacket callback + // see ICS-30 middleware for more information +} +``` + +Note that interchain accounts controller modules should not execute any logic upon packet receipt, i.e. the `OnRecvPacket` callback should not be called, and in case it is called, it should simply return an error acknowledgement: + +```typescript +// Called on Controller Chain by Relayer +function onRecvPacket(packet Packet) { + return NewErrorAcknowledgement(ErrInvalidChannelFlow) +} +``` + +### Identifier formats + +These are the default formats that the port identifiers on each side of an interchain accounts channel. The controller portID **must** include the owner address so that when a message is sent to the controller module, the sender of the message can be verified against the portID before sending the ICA packet. The controller chain is responsible for proper access control to ensure that the sender of the ICA message has successfully authenticated before the message reaches the controller module. + +Controller Port Identifier: optional prefix `icacontroller-` + mandatory `{owner-account-address}` + +Host Port Identifier: `icahost` + +The `icacontroller-` prefix on the controller port identifier is optional and host chains **must** not enforce that the counterparty port identifier includes it. Controller chains may decide to include it and validate that it is present in their own port identifier. + +## Example Implementations + +- Implementation of ICS 27 in Go can be found in [ibc-go repository](https://github.com/cosmos/ibc-go). + +## Future Improvements + +A future version of interchain accounts may be greatly simplified by the introduction of an IBC channel type that is ORDERED but does not close the channel on timeouts, and instead proceeds to accept and receive the next packet. If such a channel type is made available by core IBC, Interchain accounts could require the use of this channel type and remove all logic and state pertaining to "active channels". The metadata format can also be simplified to remove any reference to the underlying connection identifiers. + +The "active channel" setting and unsetting is currently necessary to allow interchain account owners to create a new channel in case the current active channel closes during channel timeout. The connection identifiers are part of the metadata to ensure that any new channel that gets opened are established on top of the original connection. All of this logic becomes unnecessary once the channel is ordered **and** unclosable, which can only be achieved by the introduction of a new channel type to core IBC. + +## History + +Aug 1, 2019 - Concept discussed + +Sep 24, 2019 - Draft suggested + +Nov 8, 2019 - Major revisions + +Dec 2, 2019 - Minor revisions (Add more specific description & Add interchain account on Ethereum) + +July 14, 2020 - Major revisions + +April 27, 2021 - Redesign of ics27 specification + +November 11, 2021 - Update with latest changes from implementation + +December 14, 2021 - Revisions to spec based on audits and maintainer reviews + +August 1, 2023 - Implemented channel upgrades callbacks + +## Copyright + +All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0).