概要

本文档标准规定了独立链之间通过 IBC 通道进行账户管理系统的数据包结构、状态机处理逻辑以及编码细节。

背景动机

ICS-27 Interchain Accounts 描述了一种构建在 IBC 之上的跨链账户管理协议。支持 ICS-27 的链可以在其他支持 ICS-27 的链上以编程方式创建账户,并通过 IBC 交易控制这些账户(而不是使用私钥签名)。链间账户保留了普通账户的全部能力(例如质押、转账、投票),但它们改为由另一条链通过 IBC 进行管理,从而使控制链上的所有者账户始终能够完全控制其在主机链上注册的任意链间账户。

定义

  • Host Chain:注册链间账户的链。主机链会监听来自控制链的 IBC 数据包,其中包含该链间账户将要执行的指令(例如 cosmos SDK 消息)。
  • Controller Chain:在主机链上注册并控制账户的链。控制链通过向主机链发送 IBC 数据包来控制该账户。
  • Interchain Account:主机链上的一个账户。链间账户具备普通账户的全部能力。但是,它不通过私钥签名交易,而是由控制链向主机链发送 IBC 数据包,以指示该链间账户必须执行哪些交易。
  • Interchain Account Owner:控制链上的一个账户。主机链上的每个链间账户,在控制链上都有一个对应的所有者账户。
IBC 处理器接口和 IBC 中继模块接口分别按照 ICS-25 与 ICS-26 中的定义执行。

期望属性

  • 无许可:任何参与方都可以创建链间账户,而无需第三方批准(例如链治理)。注意:具体实现可以自行加入权限控制方案,但协议本身不能依赖受信任第三方的许可机制来保证安全。
  • 故障隔离:控制链不得控制由其他控制链注册的账户。例如,在控制链遭受分叉攻击时,只有由该分叉链注册的链间账户会受到影响。
  • 发送到主机链上某个链间账户的交易顺序必须被保持。交易必须按照控制链发送的顺序由链间账户执行。
  • 如果通道关闭,控制链必须能够仅通过打开新通道来重新获得对已注册链间账户的访问能力。
  • 每个链间账户都归属于控制链上的单一账户。只有控制链上的所有者账户才有权控制该链间账户。控制链负责强制执行这一逻辑。
  • 控制链必须存储其在主机链上注册并拥有的所有链间账户地址。
  • 主机链必须能够按需限制链间账户在本链上的功能(例如,主机链可以决定在该链上注册的链间账户不能参与质押)。

技术规范

通用设计

一条链可以使用链间账户协议的一个部分或两个部分(控制 与 托管)。在其他主机链上注册账户的控制链(前提是这些主机链支持链间账户)并不一定需要允许其他控制链在本链上注册账户,反之亦然。 本规范定义了注册链间账户以及发送代表所有者账户执行的交易字节的通用方式。主机链负责反序列化并执行这些交易字节,而控制链在发送数据包之前必须预先知道主机链将如何处理这些交易字节,因此这必须在通道创建期间协商完成。

控制链合约

RegisterInterchainAccount

RegisterInterchainAccount 是注册链间账户的入口。 它使用所有者账户地址生成新的 controller portID。 它会绑定到该 controller portID,并调用 04-channel ChanOpenInit。 如果该 controller portID 已被占用,则返回错误。 会发出一个 ChannelOpenInit 事件,供链下进程(例如 relayer)捕获。 该账户将在主机链的 OnChanOpenTry 步骤中完成注册。 此函数必须在给定 connection identifier 已建立 OPEN 连接后调用。 调用方必须提供完整的通道版本。该版本必须包含带完整元数据的 ICA 版本,也可以包含通道两端封装 ICA 的其他中间件版本。请注意,这将要求了解通道两端启用了哪些中间件的上下文信息。因此,建议 ICA-auth 应用自动构造 ICA 版本,并允许用户按需启用额外的中间件版本信息。
function RegisterInterchainAccount(connectionId: Identifier, owner: string, version: string) returns (error) {
}

SendTx

SendTx 用于向主机链上的链间账户发送一个包含指令(消息)的 IBC 数据包,适用于给定的链间账户所有者。
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
}

主机链合约

RegisterInterchainAccount

RegisterInterchainAccount 会在通道创建握手期间的 OnChanOpenTry 步骤中被调用。
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 会在 ExecuteTx 之前调用。 AuthenticateTx 会检查某条消息的签名者,是否为发送该 IBC 数据包所经过通道的 counterparty portID 对应的链间账户。
function AuthenticateTx(msgs []Any, connectionId string, portId string) returns (error) {
  // GetInterchainAccountAddress(portId, connectionId)
  // if interchainAccountAddress != msgSigner return error
}

ExecuteTx

执行由控制链上的所有者账户发送的每条消息。
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
}

工具函数

// 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){
}

注册与控制流程

账户注册流程

要注册链间账户,需要一个链下进程(relayer)监听 ChannelOpenInit 事件,并具备在给定连接上完成通道创建握手的能力。
  1. 控制链使用给定的链间账户所有者地址对应的 controller portID 绑定一个新的 IBC 端口。
此端口将用于为特定的所有者/链间账户对,在控制链与主机链之间创建通道。只有与已绑定端口匹配的 {owner-account-address} 对应账户,才有权通过使用该 controller portID 创建的通道发送 IBC 数据包。控制链一侧的端口注册与访问控制由各控制链自行负责强制执行。
  1. 控制链发出一个事件,表示要基于某个连接在该端口上打开新通道。
  2. 监听 ChannelOpenInit 事件的 relayer 将继续完成通道创建握手。
  3. 在主机链的 OnChanOpenTry 回调期间,将注册一个链间账户,并在状态中保存链间账户地址到所有者账户地址的映射(这用于主机链在执行时对交易进行认证)。
  4. 在控制链的 OnChanOpenAck 回调期间,会将主机链在 OnChanOpenTry 期间注册的链间账户地址记录到状态中,保存为 (controller portID, controller connectionID) -> 链间账户地址 的映射。关于如何实现这一点,请参见下文的 metadata negotiation 部分。
  5. 在控制链和主机链各自的 OnChanOpenAck 与 OnChanOpenConfirm 回调期间,会将该链间账户/所有者对的 active-channel 写入状态。

活跃通道

控制链和宿主链必须为每个已注册的跨链账户维护一个 active-channel。active-channel 会在通道创建握手过程中设置。这是一种安全机制,用于在通道关闭时,让控制链能够重新获得对宿主链上跨链账户的访问权限。 控制链上的活跃通道示例如下:
{
  // Controller Chain
  SourcePortId: `icacontroller-<owner-account-address>`,
  SourceChannelId: `<channel-id>`,
  // Host Chain
  CounterpartyPortId: `icahost`,
  CounterpartyChannelId: `<channel-id>`,
}
如果发生通道关闭,可以在原始活跃通道所使用的同一底层连接上,使用相同的端口标识符重新发起一次新的通道握手,以替换活跃通道。ICS-27 通道只能在超时事件发生时关闭(如果实现使用有序通道),或在极少见的轻客户端攻击事件中关闭。控制链必须保留为特定 portID(包含 {owner-account-address})和 connectionID 组合重新打开新的 ICS-27 通道并重置活跃通道的能力。 控制链和宿主链必须验证任何新通道是否保持与先前活跃通道相同的元数据,以确保即使替换活跃通道,跨链账户的参数也保持不变。元数据中的 Address 不应被校验,因为在 INIT 阶段它预期为空;宿主链会在 TRY 阶段重新生成完全相同的地址,因为它预期会基于控制链 portID 和 connectionID 以确定性方式生成跨链账户地址(这两者都必须保持不变)。

元数据协商

ICS-27 利用 ICS-04 通道版本协商,在通道握手期间协商元数据和通道参数。元数据将包含编码格式以及交易类型,以便对手方就跨链交易的结构和编码达成一致。宿主链在 TRY 步骤发送的元数据还会包含跨链账户地址,以便将其中继回控制链。在通道握手结束时,控制链和宿主链都会存储一个从(控制链 portID、控制链/宿主链 connectionID)到新注册的跨链账户地址的映射(账户注册流程)。 ICS-04 允许每次通道版本协商都由应用自行定义。对于跨链账户,通道版本将是一个 JSON 结构体的字符串,其中包含所有相关元数据,用于在通道握手阶段中继给对手方(见下方总结)。 结合每个跨链账户仅对应一个通道的做法,这种元数据协商方式使我们能够在 OnChanOpenAck 回调期间,将跨链账户地址传回控制链,并创建从(控制链 portID、控制链 connection ID)到跨链账户地址的映射。正如控制流程中所述,控制链需要知道已注册跨链账户的地址,才能向宿主链上的该账户发送交易。

元数据协商总结

interchain-account-address 是控制链在宿主链上注册的跨链账户地址。
  • INIT
发起方:控制链 数据报:ChanOpenInit 作用链:控制链 版本:
{
  "Version": "ics27-1",
  "ControllerConnectionId": "self_connection_id",
  "HostConnectionId": "counterparty_connection_id",
  "Address": "",
  "Encoding": "requested_encoding_type",
  "TxType": "requested_tx_type",
}
说明:地址留空,因为它将由宿主链生成并回传。必须包含连接标识符,以确保当需要打开新通道时(例如活跃通道超时),我们可以保证新通道仍在同一连接上打开。这样可以确保跨链账户始终连接到同一个对手链。
  • TRY
发起方:中继者 数据报:ChanOpenTry 作用链:宿主链 版本:
{
  "Version": "ics27-1",
  "ControllerConnectionId": "counterparty_connection_id",
  "HostConnectionId": "self_connection_id",
  "Address": "interchain_account_address",
  "Encoding": "negotiated_encoding_type",
  "TxType": "negotiated_tx_type",
}
说明:宿主链上的 ICS-27 应用负责根据控制链在 INIT 中设置的对手方版本返回该版本。宿主链必须接受控制链请求的单一编码类型和单一 tx 类型(即包含在对手方版本中的内容)。如果请求的编码或 tx 类型不受支持,则宿主链必须返回错误并中止握手。 宿主链还必须生成跨链账户地址,并将该地址字符串填入版本中的 address 字段。
  • ACK
发起方:中继者 数据报:ChanOpenAck 作用链:控制链 对手方版本:
{
  "Version": "ics27-1",
  "ControllerConnectionId": "self_connection_id",
  "HostConnectionId": "counterparty_connection_id",
  "Address": "interchain_account_address",
  "Encoding": "negotiated_encoding_type",
  "TxType": "negotiated_tx_type",
}
说明:在 ChanOpenAck 步骤中,控制链上的 ICS27 应用必须校验宿主链在 ChanOpenTry 中选择的版本字符串。控制链必须验证自己是否支持宿主链选定的协商编码和 tx 类型。如果任一项不受支持,则必须返回错误并中止握手。 如果两者都受支持,则控制链必须存储从该通道的 portID 到所提供跨链账户地址的映射,并成功返回。

控制流程

一旦跨链账户在宿主链上注册完成,控制链就可以开始向宿主链发送指令(消息)来控制该账户。
  1. 控制链调用 SendTx,并传入将由关联跨链账户在宿主侧执行的消息(由控制侧端口标识符确定)
Cosmos SDK 伪代码示例:
// 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)
  1. 宿主链在接收到 IBC 数据包后,将调用 DeserializeTx。
  2. 宿主链随后会对每条消息调用 AuthenticateTx 和 ExecuteTx,并返回一个包含成功或错误信息的确认。
消息在宿主链上的认证方式是:获取控制侧端口标识符,并调用 GetInterchainAccountAddress(controllerPortId, hostConnectionId),以得到当前控制端口和连接标识符对应的预期跨链账户地址。如果该消息的签名者与预期账户地址不匹配,则认证失败。

数据包数据

InterchainAccountPacketData 包含跨链账户可执行的消息数组、发送到宿主链的 memo 字符串,以及数据包 type。ICS-27 第 1 版只有一种类型:EXECUTE_TX。
message InterchainAccountPacketData  {
  enum type
  bytes data = 1;
  string memo = 2;
}
确认数据包结构定义见 ics4。如果宿主链上发生错误,确认中将包含错误信息。
message Acknowledgement {
  // response contains either a result or an error and must be non-empty
  oneof response {
    bytes  result = 21;
    string error  = 22;
  }
}

自定义逻辑

ICS-27 依赖 ICS-30 中间件架构,为应用开发者提供在 ICS-27 数据包成功或失败时应用自定义逻辑的能力。 控制链会包装 OnAcknowledgementPacket 和 OnTimeoutPacket,以处理 ICS-27 数据包的成功或失败场景。

端口与通道设置

宿主链上的跨链账户模块必须始终绑定到 id 为 icahost 的端口。控制链则会按照标识符格式一节中的说明动态绑定端口。 下面的示例假设某个模块实现了完整的 InterchainAccountModule 接口。setup 函数必须在模块创建时且仅调用一次(例如在区块链自身初始化时),以绑定到合适的端口。
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)
}
调用 setup 函数后,即可通过 IBC 路由模块创建通道。

通道生命周期管理

当且仅当通道初始化步骤是由控制链发起调用时,跨链账户模块才会接受来自另一台机器上任意模块的新通道。
// 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
}
// 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
}
// 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)
}
// 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)
}
// The controller portID must have the format: `icacontroller-{ownerAddress}`
function validateControllerPortParams(portIdentifier: Identifier) {
  split(portIdentifier, "-")
  abortTransactionUnless(portIdentifier[0] === "icacontroller")
  abortTransactionUnless(IsValidAddress(portIdentifier[1]))
}

关闭握手

function onChanCloseInit(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
 	// disallow user-initiated channel closing for interchain account channels
  abortTransactionUnless(FALSE)
}
function onChanCloseConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
}

升级握手

// 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
}
// 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
}
// 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
}
// Called on Controller and Host Chains by Relayer
function onChanUpgradeOpen(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
    // no-op
} 

数据包中继

当发送到该模块的数据包被接收时,路由模块会调用 onRecvPacket。
// 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.destPort, packet.destChannel, 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。
// 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
}
// Called on Controller Chain by Relayer
function onTimeoutPacket(packet: Packet) {
  // call underlying app's OnTimeoutPacket callback 
  // see ICS-30 middleware for more information
}
请注意,跨链账户控制器模块不应在接收数据包时执行任何逻辑,也就是说,不应调用 OnRecvPacket 回调;如果它被调用,也应仅返回一个错误确认:
// Called on Controller Chain by Relayer
function onRecvPacket(packet Packet) {
  return NewErrorAcknowledgement(ErrInvalidChannelFlow)
}

标识符格式

以下是跨链账户通道两侧端口标识符的默认格式。控制器端口 ID 必须 包含所有者地址,这样当消息发送到控制器模块时,就可以在发送 ICA 数据包之前,根据 portID 校验消息发送者。控制器链负责实现适当的访问控制,以确保 ICA 消息的发送者在消息到达控制器模块之前已经成功完成身份验证。 控制器端口标识符:可选前缀 icacontroller- + 必选 {owner-account-address} 主机端口标识符:icahost 控制器端口标识符上的 icacontroller- 前缀是可选的,主机链不得强制要求对手方端口标识符包含该前缀。控制器链可以自行决定是否包含该前缀,并校验其自身端口标识符中是否存在该前缀。

实现示例

未来改进

未来版本的跨链账户可以通过引入一种 IBC 通道类型而大幅简化:该通道是 ORDERED,但在超时时不会关闭通道,而是继续接受并接收下一个数据包。如果核心 IBC 提供了这种通道类型,跨链账户就可以要求使用该通道类型,并移除所有与“活跃通道”相关的逻辑和状态。元数据格式也可以简化,删除对底层连接标识符的所有引用。 当前之所以需要设置和取消设置“活跃通道”,是为了允许跨链账户所有者在当前活跃通道因通道超时而关闭时创建新通道。连接标识符之所以是元数据的一部分,是为了确保任何新打开的通道都建立在原始连接之上。一旦通道既是有序的 并且 不可关闭,这些逻辑就都不再必要,而这只有通过在核心 IBC 中引入一种新的通道类型才能实现。

历史

2019 年 8 月 1 日 - 概念讨论 2019 年 9 月 24 日 - 提出草案 2019 年 11 月 8 日 - 重大修订 2019 年 12 月 2 日 - 小幅修订(添加更具体的描述,并添加以太坊上的跨链账户) 2020 年 7 月 14 日 - 重大修订 2021 年 4 月 27 日 - 重新设计 ics27 规范 2021 年 11 月 11 日 - 根据实现的最新变更进行更新 2021 年 12 月 14 日 - 基于审计和维护者评审对规范进行修订 2023 年 8 月 1 日 - 实现通道升级回调

版权

此处所有内容均基于 Apache 2.0 许可证授权。

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 and ICS-26, 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.
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.
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.
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.
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.
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

// 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.
  1. The controller chain emits an event signaling to open a new channel on this port given a connection.
  2. A relayer listening for ChannelOpenInit events will continue the channel creation handshake.
  3. 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).
  4. 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 section below for how to implement this.
  5. During the OnChanOpenAck & OnChanOpenConfirm callbacks on the controller & host chains respectively, the active-channel 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:
{
  // Controller Chain
  SourcePortId: `icacontroller-<owner-account-address>`,
  SourceChannelId: `<channel-id>`,
  // Host Chain
  CounterpartyPortId: `icahost`,
  CounterpartyChannelId: `<channel-id>`,
}
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 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). 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). 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, 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:
{
  "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:
{
  "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:
{
  "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:
// 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)
  1. The host chain upon receiving the IBC packet will call DeserializeTx.
  2. 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.
message InterchainAccountPacketData  {
  enum type
  bytes data = 1;
  string memo = 2;
}
The acknowledgment packet structure is defined as in ics4. If an error occurs on the host chain the acknowledgment contains the error message.
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 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. 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.
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.
// 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
}
// 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
}
// 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)
}
// 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)
}
// 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

function onChanCloseInit(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
 	// disallow user-initiated channel closing for interchain account channels
  abortTransactionUnless(FALSE)
}
function onChanCloseConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
}

Upgrade handshake

// 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
}
// 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
}
// 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
}
// 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.
// 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.destPort, packet.destChannel, 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.
// 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
}
// 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:
// 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

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 All content herein is licensed under Apache 2.0.