概述

本标准文档规定了 IBC 实现必须实现的接口与状态机逻辑,以便现有通道能够在初始通道握手完成后进行升级。

动机

随着新特性不断加入 IBC,链可能希望在不放弃现有通道已经积累的状态和网络效应的前提下,利用新的通道特性。所提出的升级协议将允许链重新协商一个现有通道,以利用新特性,而无需创建新通道,从而保留该通道上已处理的所有现有数据包状态。

期望属性

  • 双方链都 MUST 同意重新协商后的通道参数。
  • 双方链上的通道状态与逻辑 SHOULD 要么使用旧参数,要么使用新参数,但 MUST NOT 处于中间状态。例如,MUST NOT 出现某个应用运行 v2 逻辑,而其对手方仍在运行 v1 逻辑的情况。
  • 通道升级协议是原子的,即,
    • 要么升级失败,此时通道 MUST 回退到原始通道参数;
    • 要么升级成功,此时双方通道端 MUST 采用新的通道参数,应用必须据此正确处理数据包数据。
  • 在先前协商参数下发送的数据包,必须按先前协商的参数处理;在新协商参数下发送的数据包,必须按新协商的参数处理。因此,在升级握手完成之前发送的传输中数据包将按照原始参数进行处理。
  • 通道升级协议 MUST NOT 修改通道标识符。

技术规范

数据结构

ChannelState 和 ChannelEnd 定义于 ICS-4 中,此处为了便于读者阅读而再次列出。FLUSHING 和 FLUSHCOMPLETE 是为支持升级功能而新增的状态。

ChannelState

enum ChannelState {
  INIT,
  TRYOPEN,
  OPEN,
  FLUSHING,
  FLUSHCOMPLETE,
}
  • 在 ChanUpgradeInit 中,提出升级的发起链应当存储通道升级信息。
  • 执行 ChanUpgradeTry 并接受升级的对手方链应当存储通道升级信息,将通道状态从 OPEN 设置为 FLUSHING,并通过存储升级超时来启动 flush 计时器。
  • 一旦发起链验证对手方处于 FLUSHING 状态,它也必须切换到 FLUSHING,除非其本端所有传输中数据包都已经 flush 完成,在这种情况下它必须直接切换到 FLUSHCOMPLETE。发起方还将存储对手方超时信息,以确保在对手方超时过后不会切换到 FLUSHCOMPLETE。
  • 对手方链必须证明发起方同样处于 FLUSHING,或者已经在 FLUSHCOMPLETE 中完成 flush。对手方将存储发起方超时信息,以确保在发起方超时过后不会切换到 FLUSHCOMPLETE。
FLUSHING 是一种“阻塞”状态,它会阻止通道端推进到 FLUSHCOMPLETE,除非该通道端上的传输中数据包已经 flush 完成,并且双方通道端都已经切换到 FLUSHING。一旦双方都切换到 FLUSHCOMPLETE,中继者便可通过 ChanUpgradeOpen 在两端证明这一点,从而以新参数在两端打开通道。

ChannelEnd

interface ChannelEnd {
  state: ChannelState
  ordering: ChannelOrder
  counterpartyPortIdentifier: Identifier
  counterpartyChannelIdentifier: Identifier
  connectionHops: [Identifier]
  version: string
  upgradeSequence: uint64
}
  • state:该状态由升级协议的握手步骤定义,并会在握手期间原地变更。当通道端正在 flush 传输中数据包时,它将处于 FLUSHING 模式。一旦不再存在传输中数据包,且 channelEnd 已准备好切换到 OPEN,状态将变为 FLUSHCOMPLETE。
  • upgradeSequence:升级序列将在升级握手期间递增并达成一致,并会被原地更新。
在升级握手完成之前,所有其他参数在升级握手期间都将保持不变。当通道在成功完成升级握手后被重置为 OPEN 时,通道端上的字段将切换为 Upgrade 中指定的 UpgradeFields。

UpgradeFields

interface UpgradeFields {
  version: string
  ordering: ChannelOrder
  connectionHops: [Identifier]
}
MAY BE MODIFIED:
  • version:版本 MAY 由升级协议修改。初始通道握手中使用的相同版本协商机制也可以用于升级握手。
  • ordering:排序方式 MAY 由升级协议修改,前提是底层连接支持新的排序方式。
  • connectionHops:connectionHops MAY 由升级协议修改。
MUST NOT BE MODIFIED:
  • counterpartyChannelIdentifier:对手方通道标识符 MUST NOT 由升级协议修改。
  • counterpartyPortIdentifier:对手方端口标识符 MUST NOT 由升级协议修改。
注意:如果升级为 ChannelEnd 增加了任何字段,则这些字段默认是可修改的,并且可以由有权限发起升级的 Actor(例如链治理)任意选择。

Timeout

interface Timeout {
  timeoutHeight: Height
  timeoutTimestamp: uint64
}
  • timeoutHeight:超时高度表示对手方不得再继续执行升级握手的区块高度。此时双方链将保留其原始通道,并中止升级握手。
  • timeoutTimestamp:超时时间戳表示在对手方链上的某个时间点之后,对手方不得再继续执行升级握手。此时双方链将保留其原始通道,并中止升级握手。
timeoutHeight 或 timeoutTimestamp 中至少有一个 MUST 为非零值。

Upgrade

升级类型表示通道端上的一次特定升级尝试。
interface Upgrade {
  fields: UpgradeFields
  timeout: Timeout
  nextSequenceSend: uint64
}
升级对象包含执行链上该通道端的提议升级内容、此次升级尝试的超时信息,以及该通道的下一个发送数据包序列。nextSequenceSend 使对手方能够知道,在通道可以使用新协商参数重新打开之前,哪些数据包需要先被 flush。任何发送到该通道端且数据包序列大于或等于 nextSequenceSend 的数据包,在升级完成前都会被拒绝。nextSequenceSend 还将用于在对手方为新的升级重新打开时设置新的序列。

ErrorReceipt

interface ErrorReceipt {
  sequence: uint64
  errorMsg: string
}
  • sequence 包含发生错误时的 upgradeSequence。
  • errorMsg 包含一个任意字符串,链可用其提供升级为何被中止的附加信息。

存储路径

通道升级路径

链在发起升级时必须存储提议的升级内容。提议的升级必须存储在可证明存储中。一旦升级成功或已中止,该记录即可删除。
function channelUpgradePath(portIdentifier: Identifier, channelIdentifier: Identifier): Path {
  return "channelUpgrades/upgrades/ports/{portIdentifier}/channels/{channelIdentifier}"
}
升级路径在连接接口中新增了关联的成员验证方法,以便对手方验证该链已经存储并承诺了一组特定的升级参数。
// Connection VerifyChannelUpgrade method
function verifyChannelUpgrade(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  upgrade: Upgrade
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(
    connection.counterpartyPrefix, 
    channelUpgradePath(counterpartyPortIdentifier, counterpartyChannelIdentifier)
  )
  return verifyMembership(clientState, height, 0, 0, proof, path, upgrade)
}

CounterpartyUpgrade 路径

链必须在 chanUpgradeAck 和 chanUpgradeConfirm 时存储对手方升级信息。该信息将存储在私有存储中的 counterpartyUpgrade 路径下。
function counterpartyUpgradePath(portIdentifier: Identifier, channelIdentifier: Identifier): Path {
    return "channelUpgrades/counterpartyUpgrade/ports/{portIdentifier}/channels/{channelIdentifier}"
}

升级错误路径

升级错误路径是一个公开路径,可在给定升级尝试中向对手方发出升级错误信号。在成功情况下它不会存储任何内容,但如果某条链不接受提议的升级,则会存储 ErrorReceipt。
function channelUpgradeErrorPath(portIdentifier: Identifier, channelIdentifier: Identifier): Path {
    return "channelUpgrades/upgradeError/ports/{portIdentifier}/channels/{channelIdentifier}"
}
升级错误 MUST 在连接接口中新增关联的成员验证和非成员验证函数,以便对手方验证该链已在升级错误路径中存储了一个非空错误。
// Connection VerifyChannelUpgradeError method
function verifyChannelUpgradeError(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  upgradeErrorReceipt: ErrorReceipt
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(
    connection.counterpartyPrefix, 
    channelUpgradeErrorPath(counterpartyPortIdentifier, counterpartyChannelIdentifier)
  )
  return verifyMembership(clientState, height, 0, 0, proof, path, upgradeErrorReceipt)
}

子协议

通道升级过程由以下子协议组成:initUpgradeHandshake、startFlushUpgradeHandshake、openUpgradeHandshake、cancelChannelUpgrade 和 timeoutChannelUpgrade。如果双方链都批准所提议的升级,则升级握手协议应成功完成,并且 ChannelEnd 应在 OPEN 状态下升级到新参数。

实用函数

initUpgradeHandshake 是一个子协议,用于为升级握手初始化通道端。它会校验升级参数并存储通道升级。所有数据包处理将继续按照原始通道参数进行,因为这是一种可以无限期保留的信号机制。新提议的升级将被存储在可证明存储中,供对手方验证。如果在握手开始前再次调用它,则当前提议的升级将被新的升级替换,并且通道升级序列将递增。
// initUpgradeHandshake will verify that the channel is in the
// correct precondition to call the initUpgradeHandshake protocol.
// it will verify the new upgrade field parameters, and make the
// relevant state changes for initializing a new upgrade:
// - store channel upgrade
// - incrementing upgrade sequence
function initUpgradeHandshake(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  proposedUpgradeFields: UpgradeFields,
): uint64 {
  // current channel must be OPEN
  // If channel already has an upgrade but isn't in FLUSHING,
  // then this will override the previous upgrade attempt
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === OPEN)

  // new channel version must be nonempty
  abortTransactionUnless(proposedUpgradeFields.Version !== "")

  // proposedConnection must exist and be in OPEN state for 
  // channel upgrade to be accepted
  proposedConnection = provableStore.get(connectionPath(proposedUpgradeFields.connectionHops[0]))
  abortTransactionUnless(proposedConnection !== null && proposedConnection.state === OPEN)

  // new order must be supported by the new connection
  abortTransactionUnless(isSupported(proposedConnection, proposedUpgradeFields.ordering))

  // nextSequenceSend and timeout will be filled when we move to FLUSHING
  upgrade = Upgrade{
    fields: proposedUpgradeFields,
  }

  // store upgrade in provable store for counterparty proof verification
  provableStore.set(channelUpgradePath(portIdentifier, channelIdentifier), upgrade)

  channel.upgradeSequence = channel.upgradeSequence + 1
  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)
  return channel.upgradeSequence
}
isCompatibleUpgradeFields 会在两个升级字段结构体作为对手方彼此兼容时返回 true,否则返回 false。第一个字段必须是执行链上的升级字段,第二个字段必须是对手方的升级字段。此函数还会检查提议的连接跳是否存在、是否为 OPEN,以及是否与对手方的连接跳相互兼容。
function isCompatibleUpgradeFields(
  proposedUpgradeFields: UpgradeFields,
  counterpartyUpgradeFields: UpgradeFields,
): boolean {
  if (proposedUpgradeFields.ordering != counterpartyUpgradeFields.ordering) {
    return false
  }
  if (proposedUpgradeFields.version != counterpartyUpgradeFields.version) {
    return false
  }

  // connectionHops can change in a channel upgrade, however both sides must
  // still be each other's counterparty. Since connection hops may be provided
  // by relayer, we will abort to avoid changing state based on relayer-provided value
  // Note: If the proposed connection came from an existing upgrade, then the 
  // off-chain authority is responsible for replacing one side's upgrade fields
  // to be compatible so that the upgrade handshake can proceed
  proposedConnection = provableStore.get(connectionPath(proposedUpgradeFields.connectionHops[0]))
  if (proposedConnection == null || proposedConnection.state != OPEN) {
    return false
  }
  if (counterpartyUpgradeFields.connectionHops[0] != proposedConnection.counterpartyConnectionIdentifier) {
    return false
  }
  return true
}
startFlushUpgradeHandshake 会阻止升级继续进行,直到所有传输中的数据包都已完成冲刷。它会将通道状态设置为 FLUSHING,并阻止 sendPacket。在此期间,receivePacket、acknowledgePacket 和 timeoutPacket 仍然被允许,并将按照原始通道参数进行处理。状态机会设置一个计时器,用于限制对端在完成冲刷并进入 FLUSHCOMPLETE 之前可花费的时间。新提议的升级将被存储在公共存储中,供对手方验证。
// startFlushUpgradeHandshake will verify that the channel
// is in a valid precondition for calling the startFlushUpgradeHandshake.
// it will set the channel to flushing state.
// it will store the nextSequenceSend and upgrade timeout in the upgrade state.
function startFlushUpgradeHandshake(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
) {
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === OPEN)

  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  abortTransactionUnless(upgrade !== null)

  channel.state = FLUSHING

  upgradeTimeout = getUpgradeTimeout(channel.portIdentifier, channel.channelIdentifier)
  // either timeout height or timestamp must be non-zero
  abortTransactionUnless(upgradeTimeout.timeoutHeight != 0 || upgradeTimeout.timeoutTimestamp != 0)

  nextSequenceSend = provableStore.get(nextSequenceSendPath(portIdentifier, channelIdentifier))

  upgrade.timeout = upgradeTimeout
  upgrade.nextSequenceSend = nextSequenceSend
  
  // store upgrade in public store for counterparty proof verification
  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)
  provableStore.set(channelUpgradePath(portIdentifier, channelIdentifier), upgrade)
}
openUpgradeHandshake 会打开通道,并将现有通道参数切换为新达成一致的升级后通道字段。
// openUpgradeHandshake will switch the channel fields 
// over to the agreed upon upgrade fields.
// it will reset the channel state to OPEN.
// it will delete auxiliary upgrade state.
// caller must do all relevant checks before calling this function.
function openUpgradeHandshake(
  portIdentifier: Identifier,
  channelIdentifier: Identifier
) {
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))

  // if channel order changed, we need to set
  // the recv and ack sequences appropriately
  if channel.order == "UNORDERED" && upgrade.fields.ordering == "ORDERED" {
    selfNextSequenceSend = provableStore.get(nextSequenceSendPath(portIdentifier, channelIdentifier))
    counterpartyUpgrade = privateStore.get(counterpartyUpgradePath(portIdentifier, channelIdentifier))

    // set nextSequenceRecv to the counterparty nextSequenceSend since all packets were flushed
    provableStore.set(nextSequenceRecvPath(portIdentifier, channelIdentifier), counterpartyUpgrade.nextSequenceSend)
    // set nextSequenceAck to our own nextSequenceSend since all packets were flushed
    provableStore.set(nextSequenceAckPath(portIdentifier, channelIdentifier), selfNextSequenceSend)
  } else if channel.order == "ORDERED" && upgrade.fields.ordering == "UNORDERED" {
    // reset recv and ack sequences to 1 for UNORDERED channel
    provableStore.set(nextSequenceRecvPath(portIdentifier, channelIdentifier), 1)
    provableStore.set(nextSequenceAckPath(portIdentifier, channelIdentifier), 1)
  }

  // switch channel fields to upgrade fields
  // and set channel state to OPEN
  channel.ordering = upgrade.fields.ordering
  channel.version = upgrade.fields.version
  channel.connectionHops = upgrade.fields.connectionHops
  channel.state = OPEN
  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)

  // IMPLEMENTATION DETAIL: Implementations may choose to prune stale acknowledgements and receipts at this stage
  // Since flushing has completed, any acknowledgement or receipt written before the chain went into flushing has
  // already been processed by the counterparty and can be removed.
  // Implementations may do this pruning work over multiple blocks for gas reasons. In this case, they should be sure
  // to only prune stale acknowledgements/receipts and not new ones that have been written after the channel has reopened.
  // Implementations may use the counterparty NextSequenceSend as a way to determine which acknowledgement/receipts
  // were already processed by counterparty when flushing completed

  // delete auxiliary state
  provableStore.delete(channelUpgradePath(portIdentifier, channelIdentifier))
  privateStore.delete(counterpartyUpgradePath(portIdentifier, channelIdentifier))
}
restoreChannel 会在执行中的通道需要中止升级握手并返回原始参数时,写入一个 ErrorReceipt,将通道恢复到其原始状态,并删除升级信息。
// restoreChannel will restore the channel state to its pre-upgrade state
// and delete upgrade auxiliary state so that upgrade is aborted.
// it writes an error receipt to state so counterparty can restore as well.
// NOTE: this function signature may be modified by implementers to take a custom error
function restoreChannel(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
) {
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  errorReceipt = ErrorReceipt{
    channel.upgradeSequence,
    "upgrade handshake is aborted", // constant string changeable by implementation
  }
  provableStore.set(channelUpgradeErrorPath(portIdentifier, channelIdentifier), errorReceipt)
  channel.state = OPEN
  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)

  // delete auxiliary state
  provableStore.delete(channelUpgradePath(portIdentifier, channelIdentifier))
  privateStore.delete(counterpartyUpgradePath(portIdentifier, channelIdentifier))
}
pendingInflightPackets 将返回从该 ChannelEnd 发送的传输中数据包序列列表。由于数据包生命周期完成时会删除其数据包承诺,因此可以监控这一点。也就是说,如果发送方链上仍存在该数据包承诺,则说明该数据包生命周期尚未完成。本规范未提供伪代码,因为这将依赖于具体的状态机。ibc-go 实现将使用存储迭代器来实现该功能。函数签名如下:
// pendingInflightPacketSequences returns the packet sequences sent on 
// this end that have not had their lifecycle completed
function pendingInflightPacketSequences(
  portIdentifier: Identifier,
  channelIdentifier: Identifier
): [uint64]
isAuthorizedUpgrader 会在所提供地址被授权初始化、修改和取消升级时返回 true。链可以为一组地址授予权限,以表明通道愿意升级到哪一种升级方案。
// isAuthorizedUpgrader
function isAuthorizedUpgrader(address: string): boolean
getUpgradeTimeout 将返回为给定通道指定的升级超时时间。它可以是链范围参数,也可以是按通道选择的参数。这是一个实现层面的细节,因此这里只指定函数签名。注意,这应当为该通道获取某个已存储的超时增量,并将其加到当前高度和时间上,以得到绝对超时值。
// getUpgradeTimeout
function getUpgradeTimeout(portIdentifier: string, channelIdentifier: string) Timeout {
}

升级握手

升级握手定义了七种数据报:ChanUpgradeInit、ChanUpgradeTry、ChanUpgradeAck、ChanUpgradeConfirm、ChanUpgradeOpen、ChanUpgradeTimeout 和 ChanUpgradeCancel 一次成功的协议执行流程如下(注意,所有调用都根据 ICS 25 通过模块进行):
发起方数据报执行操作的链先前状态 (A, B)后续状态 (A, B)
ActorChanUpgradeInitA(OPEN, OPEN)(OPEN, OPEN)
RelayerChanUpgradeTryB(OPEN, OPEN)(OPEN, FLUSHING)
RelayerChanUpgradeAckA(OPEN, FLUSHING)(FLUSHING/FLUSHCOMPLETE, FLUSHING)
RelayerChanUpgradeConfirmB(FLUSHING/FLUSHCOMPLETE, FLUSHING)(FLUSHING/FLUSHCOMPLETE, FLUSHING/FLUSHCOMPLETE/OPEN)
重要: 请注意,信道升级流程开始之前的前置状态必须是双方信道端都处于 OPEN。如果某一端在开始信道升级之前的前置状态不是 OPEN,则被授权的升级执行者会面临信道在升级过程中停滞的风险。 可参考下图了解一种可能的信道升级流程。在第 5 步和第 7 步中展示了多个信道状态,因为在执行握手后,信道端可能会进入这些可能状态之一。注意,在此示例中,链 B 上的信道端会在 ChanUpgradeConfirm(第 7 步)时使用新参数进入 OPEN。 信道升级流程 一旦双方状态都进入 FLUSHING,并且双方都已存储对方的升级超时,双方即可通过清空其在途数据包进入 FLUSHCOMPLETE。一旦双方都完成清空,relayer 就可以向两端提交 ChanUpgradeOpen 数据报,并证明对手方也已完成清空,从而将 channelEnd 迁移到 OPEN。 只有当链 B 没有在 ChanUpgradeConfirm 时迁移到 OPEN 时,才需要在链 B 上调用 ChanUpgradeOpen;如果两端的所有数据包都已经清空,就可能发生这种情况。 在两个实现该子协议的链之间成功完成一次升级握手后,将满足以下属性:
  • 每条链都在运行其新升级后的信道端,并根据升级后的参数处理升级后的逻辑与状态。
  • 每条链都知晓并同意对手方升级后的信道参数。
  • 所有在握手之前发送的数据包,都已使用旧参数被完全清空(已确认或已超时)。
  • 所有在某个信道端迁移到 OPEN 之后发送的数据包,要么会在发送侧 channelEnd 上使用新参数超时,要么会被对手方使用新参数接收。
如果某条链不同意提议的对手方升级后 ChannelEnd,它可以通过在 channelUpgradeErrorPath 写入一个 ErrorReceipt 并恢复原始信道来中止升级握手。该 ErrorReceipt 必须包含出错链的信道端上的当前升级序列。 channelUpgradeErrorPath(portID, channelID) => ErrorReceipt(sequence, msg) 随后,relayer 可以向对手方提交一个 ChanUpgradeCancel 数据报。链在收到该消息后,必须验证对手方是否已在其 channelUpgradeErrorPath 写入一个 ErrorReceipt,且其中的序列号大于或等于本方 ChannelEnd 的升级序列。如果验证成功,它也会恢复自己的原始信道,从而取消此次升级。 如果某条链未能在对手方指定的超时时间内达到 FLUSHCOMPLETE,那么它不得迁移到 FLUSHCOMPLETE,而应中止升级。relayer 可以在 ChanUpgradeTimeout 数据报中向对手方链提交这一事实的证明,使对手方也取消升级并恢复其原始信道。
// Channel Ends on both sides **must** be OPEN before this function is called
// It is the responsibility of the authorized upgrader to ensure this is the case
function chanUpgradeInit(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  proposedUpgradeFields: UpgradeFields,
  msgSender: string,
) {
  // chanUpgradeInit may only be called by addresses authorized by executing chain
  abortTransactionUnless(isAuthorizedUpgrader(msgSender))

  // if a previous upgrade attempt exists, then delete it and write error receipt, so
  // counterparty can abort it and move to next upgrade
  existingUpgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  if existingUpgrade != null {
    provableStore.delete(channelUpgradePath(portIdentifier, channelIdentifier))
    errorReceipt = ErrorReceipt{
      channel.upgradeSequence,
      "abort the previous upgrade attempt so counterparty can accept the new one", // constant string changeable by implementation
    }
    provableStore.set(channelUpgradeErrorPath(portIdentifier, channelIdentifier), errorReceipt)
  }

  upgradeSequence = initUpgradeHandshake(portIdentifier, channelIdentifier, proposedUpgradeFields)

  // call modules onChanUpgradeInit callback
  // onChanUpgradeInit may return a new proposed version
  // if an error is returned the upgrade is not written
  // the callback MUST NOT write state, as all state transitions will occur once
  // the channel upgrade is complete.
  module = lookupModule(portIdentifier)
  version, err = module.onChanUpgradeInit(
    portIdentifier,
    channelIdentifier,
    upgradeSequence,
    proposedUpgradeFields.ordering,
    proposedUpgradeFields.connectionHops,
    proposedUpgradeFields.version
  )
  // abort transaction if callback returned error
  abortTransactionUnless(err === null)

  // replace channel upgrade version with the version returned by application
  // in case it was modified
  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  upgrade.fields.version = version
  provableStore.set(channelUpgradePath(portIdentifier, channelIdentifier), upgrade)
}
注意:各个实现应如何为 chanUpgradeInit 函数提供访问控制,由具体实现自行决定。例如链上治理、许可参与者、DAO 等。
function chanUpgradeTry(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyUpgrade: Upgrade,
  counterpartyUpgradeSequence: uint64,
  proposedConnectionHops: [Identifier],
  proofChannel: CommitmentProof,
  proofUpgrade: CommitmentProof,
  proofHeight: Height
) {
  // current channel must be OPEN (i.e. not in FLUSHING)
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === OPEN)

  // construct counterpartyChannel from existing information and provided
  // counterpartyUpgradeSequence
  counterpartyChannel = ChannelEnd{
    state: OPEN,
    ordering: channel.ordering,
    counterpartyPortIdentifier: portIdentifier,
    counterpartyChannelIdentifier: channelIdentifier,
    connectionHops: counterpartyHops,
    version: channel.version,
    sequence: counterpartyUpgradeSequence,
  }

  // verify proofs of counterparty state
  abortTransactionUnless(
    verifyChannelState(
      connection,
      proofHeight,
      proofChannel,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyChannel
    )
  )
  abortTransactionUnless(
    verifyChannelUpgrade(
      connection,
      proofHeight,
      proofUpgrade,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyUpgrade
    )
  )

  existingUpgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  if existingUpgrade != null {
    expectedUpgradeSequence = channel.UpgradeSequence
  } else {
    // at the end of the TRY step, the current upgrade sequence will be incremented in the non-crossing
    // hello case due to calling chanUpgradeInit, we should use this expected upgrade sequence for
    // sequence mismatch comparison
    expectedUpgradeSequence = channel.UpgradeSequence + 1
  }

  // NON CROSSING HELLO CASE:
  // if the counterparty sequence is less than or equal to the current sequence,
  // then either the counterparty chain is out-of-sync or the message
  // is out-of-sync and we write an error receipt with our sequence
  // so that the counterparty can abort their attempt and resync with our sequence.
  // When the next upgrade attempt is initiated, both sides will move to a fresh
  // never-before-seen sequence number
  // CROSSING HELLO CASE:
  // if the counterparty sequence is less than the current sequence,
  // then either the counterparty chain is out-of-sync or the message
  // is out-of-sync and we write an error receipt with our sequence minus one
  // so that the counterparty can update their sequence as well.
  // This will cause the outdated counterparty to upgrade the sequence
  // and abort their out-of-sync upgrade without aborting our own since
  // the error receipt sequence is lower than ours and higher than the counterparty.
  if counterpartyUpgradeSequence < expectedUpgradeSequence {
    errorReceipt = ErrorReceipt{
      expectedUpgradeSequence - 1,
      "sequence out of sync", // constant string changeable by implementation
    }
    provableStore.set(channelUpgradeErrorPath(portIdentifier, channelIdentifier), errorReceipt)
    return
  }
  
  // create upgrade fields for this chain from counterparty upgrade and 
  // relayer-provided information version may be mutated by application callback
  upgradeFields = Upgrade{
    ordering: counterpartyUpgrade.fields.ordering,
    connectionHops: proposedConnectionHops,
    version: counterpartyUpgrade.fields.version,
  }

  // current upgrade either doesn't exist (non-crossing hello case),
  // we initialize the upgrade with constructed upgradeFields
  // if it does exist, we are in crossing hellos and must assert
  // that the upgrade fields are the same for crossing-hellos case
  if (existingUpgrade == null) {
    initUpgradeHandshake(portIdentifier, channelIdentifier, upgradeFields)
  } else {
    // we must use the existing upgrade fields
    upgradeFields = existingUpgrade.fields
  }

  abortTransactionUnless(isCompatibleUpgradeFields(upgradeFields, counterpartyUpgradeFields))

  // if the counterparty sequence is greater than the current sequence,
  // we fast forward to the counterparty sequence so that both channel 
  // ends are using the same sequence for the current upgrade.
  // initUpgradeHandshake will increment the sequence so after that call
  // both sides will have the same upgradeSequence
  if (counterpartyUpgradeSequence > channel.upgradeSequence) {
    channel.upgradeSequence = counterpartyUpgradeSequence
  }
  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)

  // get counterpartyHops for given connection
  connection = provableStore.get(connectionPath(channel.connectionHops[0]))
  counterpartyHops = [connection.counterpartyConnectionIdentifier]

  // call startFlushUpgradeHandshake to move channel to FLUSHING, which will block
  // upgrade from progressing to OPEN until flush completes on both ends
  startFlushUpgradeHandshake(portIdentifier, channelIdentifier)

  // refresh channel to get latest state
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
// call modules onChanUpgradeTry callback // onChanUpgradeTry may return a new proposed version // if an error is returned the upgrade is not written // the callback MUST NOT write state, as all state transitions will occur once // the channel upgrade is complete. module = lookupModule(portIdentifier) version, err = module.onChanUpgradeTry( portIdentifier, channelIdentifier, channel.upgradeSequence, upgradeFields.ordering, upgradeFields.connectionHops, upgradeFields.version ) // abort the transaction if the callback returns an error and // there was no existing upgrade. This will allow the counterparty upgrade // to continue existing while this chain may add support for it in the future abortTransactionUnless(err === null) // replace channel version with the version returned by application // in case it was modified upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier)) upgrade.fields.version = version provableStore.set(channelUpgradePath(portIdentifier, channelIdentifier), upgrade) }

注意:想要显式为升级授权的实现应强制执行 crossing hellos。也就是说,双方都必须以相互兼容的参数调用过 `ChanUpgradeInit`,`ChanUpgradeTry` 才能成功。希望对由对手链发起的升级采取宽松策略的实现,可以允许在执行链此前未存储升级的情况下,从 `OPEN` 迁移到 `FLUSHING`。

```typescript
function chanUpgradeAck(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyUpgrade: Upgrade,
  proofChannel: CommitmentProof,
  proofUpgrade: CommitmentProof,
  proofHeight: Height
) {
  // current channel is OPEN or FLUSHING (crossing hellos)
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state == OPEN || channel.state == FLUSHING)

  connection = provableStore.get(connectionPath(channel.connectionHops[0]))
  counterpartyHops = [connection.counterpartyConnectionIdentifier]

  // construct counterpartyChannel from existing information
  counterpartyChannel = ChannelEnd{
    state: FLUSHING,
    ordering: channel.ordering,
    counterpartyPortIdentifier: portIdentifier,
    counterpartyChannelIdentifier: channelIdentifier,
    connectionHops: counterpartyHops,
    version: channel.version,
    sequence: channel.upgradeSequence,
  }

  // verify proofs of counterparty state
  abortTransactionUnless(
    verifyChannelState(
      connection,
      proofHeight,
      proofChannel,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyChannel
    )
  )
  abortTransactionUnless(
    verifyChannelUpgrade(
      connection,
      proofHeight,
      proofUpgrade,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyUpgrade
    )
  )

  existingUpgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))

  // optimistically accept version that TRY chain proposes and pass this to callback for confirmation.
  // in the crossing hello case, we do not modify version that our TRY call returned and instead 
  // enforce that both TRY calls returned the same version
  if (channel.state == OPEN) {
    existingUpgrade.fields.version == counterpartyUpgrade.fields.version
  }
  // if upgrades are not compatible by ACK step, then we restore the channel
  if (!isCompatibleUpgradeFields(existingUpgrade.fields, counterpartyUpgrade.fields)) {
    restoreChannel(portIdentifier, channelIdentifier)
    return
  }

  if (channel.state == OPEN) {
    // prove counterparty and move our own state to flushing
    // if we are already at flushing, then no state changes occur
    // upgrade is blocked on this channelEnd from progressing until flush completes on its end
    startFlushUpgradeHandshake(portIdentifier, channelIdentifier)
    // startFlushUpgradeHandshake sets the timeout for the upgrade
    // so retrieve upgrade again here and use that timeout value
    upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
    existingUpgrade.timeout = upgrade.timeout
  }

  timeout = counterpartyUpgrade.timeout
  
  // counterparty-specified timeout must not have exceeded
  // if it has, then restore the channel and abort upgrade handshake
  if ((timeout.timeoutHeight != 0 && currentHeight() >= timeout.timeoutHeight) ||
      (timeout.timeoutTimestamp != 0 && currentTimestamp() >= timeout.timeoutTimestamp )) {
        restoreChannel(portIdentifier, channelIdentifier)
        return
  }

  // if there are no in-flight packets on our end, we can automatically go to FLUSHCOMPLETE
  if (pendingInflightPackets(portIdentifier, channelIdentifier) == null) {
    channel.state = FLUSHCOMPLETE
  }
  // set counterparty upgrade
  privateStore.set(counterpartyUpgradePath(portIdentifier, channelIdentifier), counterpartyUpgrade)

  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)

  // call modules onChanUpgradeAck callback
  // module can error on counterparty version
  // ACK should not change state to the new parameters yet
  // as that will happen on the onChanUpgradeOpen callback
  module = lookupModule(portIdentifier)
  err = module.onChanUpgradeAck(
    portIdentifier,
    channelIdentifier,
    counterpartyUpgrade.fields.version
  )
  // restore channel if callback returned error
  if (err != null) {
    restoreChannel(portIdentifier, channelIdentifier)
    return
  }

  // if no error, agree on final version
  provableStore.set(channelUpgradePath(portIdentifier, channelIdentifier), existingUpgrade)
}
在对手链调用 chanUpgradeAck 之后,chanUpgradeConfirm 会在处于 FLUSHING 状态的链上被调用。这会将对手链在 ACK 时设置的超时通知给 TRY 链。如果超时已经到期,我们将写入错误回执并恢复。如果两侧的数据包都已经完成冲刷且超时未到,那么我们就可以打开通道。否则,我们会在私有存储中设置对手链的超时,并等待数据包冲刷完成。
function chanUpgradeConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannelState: state,
  counterpartyUpgrade: Upgrade,
  proofChannel: CommitmentProof,
  proofUpgrade: CommitmentProof,
  proofHeight: Height,
) {
  // current channel is in FLUSHING
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === FLUSHING)

  // counterparty channel is either FLUSHING or FLUSHCOMPLETE
  abortTransactionUnless(counterpartyChannelState === FLUSHING || counterpartyChannelState === FLUSHCOMPLETE)

  connection = provableStore.get(connectionPath(channel.connectionHops[0]))
  counterpartyHops = [connection.counterpartyConnectionIdentifier]

  counterpartyChannel = ChannelEnd{
    state: counterpartyChannelState,
    ordering: channel.ordering,
    counterpartyPortIdentifier: portIdentifier,
    counterpartyChannelIdentifier: channelIdentifier,
    connectionHops: counterpartyHops,
    version: channel.version,
    sequence: channel.upgradeSequence,
  }

  // verify proofs of counterparty state
  abortTransactionUnless(
    verifyChannelState(
      connection,
      proofHeight,
      proofChannel,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyChannel
    )
  )
  abortTransactionUnless(
    verifyChannelUpgrade(
      connection,
      proofHeight,
      proofUpgrade, 
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyUpgrade
    )
  )

  existingUpgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))

	// in the crossing-hello case it is possible that both chains execute the
	// INIT, TRY and CONFIRM steps without any of them executing ACK, therefore
	// we also need to check that the upgrades are compatible on this step
  if (!isCompatibleUpgradeFields(existingUpgrade.fields, counterpartyUpgrade.fields)) {
    restoreChannel(portIdentifier, channelIdentifier)
    return
  }

  timeout = counterpartyUpgrade.timeout
  
  // counterparty-specified timeout must not have exceeded
  // if it has, then restore the channel and abort upgrade handshake
  if ((timeout.timeoutHeight != 0 && currentHeight() >= timeout.timeoutHeight) ||
      (timeout.timeoutTimestamp != 0 && currentTimestamp() >= timeout.timeoutTimestamp)) {
        restoreChannel(portIdentifier, channelIdentifier)
        return
  }

  // if there are no in-flight packets on our end, we can automatically go to FLUSHCOMPLETE
  if (pendingInflightPackets(portIdentifier, channelIdentifier) == null) {
    channel.state = FLUSHCOMPLETE
    provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)
  }
  // set counterparty upgrade
  privateStore.set(counterpartyUpgradePath(portIdentifier, channelIdentifier), counterpartyUpgrade)

  // if both chains are already in flushcomplete we can move to OPEN
  if (channel.state == FLUSHCOMPLETE && counterpartyChannelState == FLUSHCOMPLETE) {
    openUpgradeHandshake(portIdentifier, channelIdentifier)
    // make application state changes based on new channel parameters
    module.onChanUpgradeOpen(portIdentifier, channelIdentifier)
  }
}
只有当双方都迁移到 FLUSHCOMPLETE 后,才能调用 chanUpgradeOpen。如果握手进入 FLUSHING 模式时队列中仍存在未处理的数据包,那么数据包处理器必须在该通道端上的最后一个数据包处理完成后,将通道端迁移到 FLUSHCOMPLETE。
function chanUpgradeOpen(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannelState: ChannelState,
  counterpartyUpgradeSequence: uint64,
  proofChannel: CommitmentProof,
  proofHeight: Height,
) {
  // channel must have completed flushing
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === FLUSHCOMPLETE)

  // get connection for proof verification
  connection = provableStore.get(connectionPath(channel.connectionHops[0]))

  // counterparty must be in OPEN or FLUSHCOMPLETE state
  if (counterpartyChannelState == OPEN) {
    // get upgrade since counterparty should have upgraded to these parameters
    upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))

    // get the counterparty's connection hops for the proposed upgrade connection
    proposedConnection = provableStore.get(connectionPath(upgrade.fields.connectionHops))
    counterpartyHops = [proposedConnection.counterpartyConnectionIdentifier]

    // The counterparty upgrade sequence must be greater than or equal to
    // the channel upgrade sequence. It should normally be equivalent, but
    // in the unlikely case that a new upgrade is initiated after it reopens,
    // then the upgrade sequence will be greater than our upgrade sequence.
    abortTransactionUnless(counterpartyUpgradeSequence >= channel.upgradeSequence)

    counterpartyChannel = ChannelEnd{
      state: OPEN,
      ordering: upgrade.fields.ordering,
      counterpartyPortIdentifier: portIdentifier,
      counterpartyChannelIdentifier: channelIdentifier,
      connectionHops: counterpartyHops,
      version: upgrade.fields.version,
      sequence: counterpartyUpgradeSequence,
    }
  } else if (counterpartyChannelState == FLUSHCOMPLETE) {
    counterpartyHops = [connection.counterpartyConnectionIdentifier]
    counterpartyChannel = ChannelEnd{
      state: FLUSHCOMPLETE,
      ordering: channel.ordering,
      counterpartyPortIdentifier: portIdentifier,
      counterpartyChannelIdentifier: channelIdentifier,
      connectionHops: counterpartyHops,
      version: channel.version,
      sequence: channel.upgradeSequence,
    }
  } else {
    abortTransactionUnless(false)
  }

  abortTransactionUnless(
    verifyChannelState(
      connection, 
      proofHeight, 
      proofChannel, 
      channel.counterpartyPortIdentifier, 
      channel.counterpartyChannelIdentifier, 
      counterpartyChannel
    )
  )

  // 将通道切换到 OPEN 并采用升级参数
  openUpgradeHandshake(portIdentifier, channelIdentifier)

  // 调用模块的 onChanUpgradeOpen 回调
  module = lookupModule(portIdentifier)
  // 由于对手方已成功升级,open 回调不得返回错误
  // 根据新的通道参数更新应用状态
  module.onChanUpgradeOpen(
    portIdentifier,
    channelIdentifier
  )
}

取消升级流程

在升级握手期间,链可以通过向升级错误路径写入错误回执并将原始通道恢复为 OPEN 来取消升级。随后,对手方也必须将其通道恢复为 OPEN。中继者可以通过向处理器发送 ChanUpgradeCancel 数据报来促成这一过程:
function cancelChannelUpgrade(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  errorReceipt: ErrorReceipt,
  proofUpgradeError: CommitmentProof,
  proofHeight: Height,
  msgSender: string,
) {
  // 当前通道已存储升级信息
  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  abortTransactionUnless(upgrade !== null)

  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  // 如果 msgSender 被授权发起和取消升级,并且
  // 当前通道尚未达到 FLUSHCOMPLETE,
  // 那么我们可以无需额外检查立即恢复
  // 否则,只有当对手方在升级握手期间写入了
  // 错误回执时,我们才能取消
  if (!(isAuthorizedUpgrader(msgSender) && channel.state != FLUSHCOMPLETE)) {
    abortTransactionUnless(!isEmpty(errorReceipt))

    if channel.state == FLUSHCOMPLETE {
      // 如果通道状态处于 FLUSHCOMPLETE,则**只能**在存在
      // 序列号完全相同的错误回执时中止。这可以确保对手方
      // 没有先成功升级,再在新的升级上取消以中止我方这一端,
      // 从而导致通道两端都处于 OPEN 但参数不同
      abortTransactionUnless(errorReceipt.sequence == channel.upgradeSequence)
    } else {
      // 如果对手方序列号小于当前序列号,
      // 则中止事务,因为该错误回执来自先前的升级
      abortTransactionUnless(errorReceipt.sequence >= channel.upgradeSequence)
    }
    // 将通道序列推进到更高的序列号,以便我们可以在新的序列号上
    // 重新开始握手
    channel.upgradeSequence = errorReceipt.sequence
    provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)

    // 获取底层连接以进行证明验证
    connection = provableStore.get(connectionPath(channel.connectionHops[0]))
    // 验证所提供的错误回执已使用对手方序列号写入 upgradeError 路径
    abortTransactionUnless(
      verifyChannelUpgradeError(
        connection,
        proofHeight,
        proofUpgradeError,
        channel.counterpartyPortIdentifier,
        channel.counterpartyChannelIdentifier,
        errorReceipt
      )
    )
  }

  // 取消升级并写入错误回执
  restoreChannel(portIdentifier, channelIdentifier)
}

升级超时流程

在尝试清空现有数据包时,通道升级过程可能会无限期停滞。为防止这种情况,每条链在进入 FLUSHING 时都会设置一个超时。如果对手方未能在预期时间窗口内完成清空,则中继者可以提交一条超时消息,将通道按原始参数恢复为 OPEN。同时还会写入错误回执,以便尚未转入 FLUSHCOMPLETE 的对手方也能按原始参数将通道恢复为 OPEN。
function timeoutChannelUpgrade(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannel: ChannelEnd,
  proofChannel: CommitmentProof,
  proofHeight: Height,
) {
  // 当前通道必须存在一个处于 FLUSHING 或 FLUSHCOMPLETE 的升级
  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  abortTransactionUnless(upgrade !== null)
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === FLUSHING || channel.state === FLUSHCOMPLETE)

  upgradeTimeout = upgrade.timeout

  // 证明必须来自超时已经过后的高度。
  // timeoutHeight 或 timeoutTimestamp 必须定义其一。
  // 如果定义了 timeoutHeight,而证明来自
  // 超时高度之前,则中止事务
  abortTransactionUnless(
    upgradeTimeout.timeoutHeight.IsZero() || 
    proofHeight >= upgradeTimeout.timeoutHeight
  )
  // 如果定义了 timeoutTimestamp,则来自证明高度的共识时间
  // 必须大于超时时间戳
  connection = provableStore.get(connectionPath(channel.connectionHops[0]))
  abortTransactionUnless(
    upgradeTimeout.timeoutTimestamp.IsZero() || 
    getTimestampAtHeight(connection, proofHeight) >= upgradeTimeout.timestamp
  )

  // 必须证明在超时经过后,对手方通道尚未完成清空
  abortTransactionUnless(counterpartyChannel.state !== FLUSHCOMPLETE)
  // 如果对手方通道状态为 OPEN,我们只应在
  // 对手方已成功完成升级时中止该事务
  if (counterpartyChannel.state == OPEN) {
    // 获取升级信息,因为对手方本应已升级到这些参数
    upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))

    // 获取提议连接的对手方 hops
    proposedConnection = provableStore.get(connectionPath(upgrade.fields.connectionHops))
    counterpartyHops = [proposedConnection.counterpartyConnectionIdentifier]

    // 检查通道是否未成功升级
    if ((upgrade.fields.version == counterpartyChannel.version) &&
        (upgrade.fields.order == counterpartyChannel.order) &&
        (counterpartyHops == counterpartyChannel.connectionHops)) {
          // 对手方已经成功升级,因此我们不能触发超时
          abortTransactionUnless(false)
    }
  }
  abortTransactionUnless(counterpartyChannel.upgradeSequence >= channel.upgradeSequence)
  abortTransactionUnless(
    verifyChannelState(
      connection,
      proofHeight,
      proofChannel,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyChannel
    )
  )

  // 由于超时验证已经通过,我们必须恢复该通道
  // 此序列号会写入错误回执,对手方可以调用 cancelUpgradeHandshake
  restoreChannel(portIdentifier, channelIdentifier)
}
如果对手方升级超时已经过去,双方都不得完成升级握手并进入 FLUSHCOMPLETE。这将防止通道两端进入不兼容的状态。

注意事项

请注意,如果在途数据包无法被成功清理,通道升级握手可能永远无法成功完成。出现这种情况的原因可能是数据包的超时值过大、确认始终未到达,或者存在某个缺陷导致确认或超时一个数据包变得不可能。在这些情况下,必须由某种协议外机制(例如治理)介入,或许通过强制清除数据包承诺来“手动”清理数据包,然后再重新启动升级握手。

迁移

链可能需要更新其内部状态,以与新的已升级通道保持一致。在这种情况下,迁移处理器应在升级流程开始前就已包含在链的二进制文件中,以便链在升级成功后能够正确迁移其状态。如果某次升级需要迁移处理器但该处理器不可用,则执行升级的链必须拒绝此次升级,以避免进入无效状态。该状态迁移不会由对手方验证,因为对手方只会假定:如果通道被升级到某个特定的通道版本,那么对手方上的辅助状态也会一并更新,以符合该通道版本的规范。迁移只能在升级成功完成且新通道处于 OPEN 后运行(即在 ChanUpgradeConfirm 或 ChanUpgradeOpen 上)。

示例实现

  • Go 语言中的通道升级实现可在 ibc-go 仓库 中找到。

历史

2024 年 2 月 1 日 - 按 ibc-go 中的实现编写的规范 2024 年 7 月 24 日 - 在 chanUpgradeConfirm 中添加升级兼容性检查

版权

此处所有内容均采用 Apache 2.0 许可。

Synopsis

This standard document specifies the interfaces and state machine logic that IBC implementations must implement in order to enable existing channels to upgrade after the initial channel handshake.

Motivation

As new features get added to IBC, chains may wish to take advantage of new channel features without abandoning the accumulated state and network effect(s) of an already existing channel. The upgrade protocol proposed would allow chains to renegotiate an existing channel to take advantage of new features without having to create a new channel, thus preserving all existing packet state processed on the channel.

Desired Properties

  • Both chains MUST agree to the renegotiated channel parameters.
  • Channel state and logic on both chains SHOULD either be using the old parameters or the new parameters, but MUST NOT be in an in-between state, e.g., it MUST NOT be possible for an application to run v2 logic, while its counterparty is still running v1 logic.
  • The channel upgrade protocol is atomic, i.e.,
    • either it is unsuccessful and then the channel MUST fall-back to the original channel parameters;
    • or it is successful and then both channel ends MUST adopt the new channel parameters and the applications must process packet data appropriately.
  • Packets sent under the previously negotiated parameters must be processed under the previously negotiated parameters, packets sent under the newly negotiated parameters must be processed under the newly negotiated parameters. Thus, in-flight packets sent before the upgrade handshake is complete will be processed according to the original parameters.
  • The channel upgrade protocol MUST NOT modify the channel identifiers.

Technical Specification

Data Structures

The ChannelState and ChannelEnd are defined in ICS-4, they are reproduced here for the reader’s convenience. FLUSHING and FLUSHCOMPLETE are additional states added to enable the upgrade feature.

ChannelState

enum ChannelState {
  INIT,
  TRYOPEN,
  OPEN,
  FLUSHING,
  FLUSHCOMPLETE,
}
  • In ChanUpgradeInit, the initializing chain that is proposing the upgrade should store the channel upgrade.
  • The counterparty chain executing ChanUpgradeTry that accepts the upgrade should store the channel upgrade, set the channel state from OPEN to FLUSHING, and start the flushing timer by storing an upgrade timeout.
  • Once the initiating chain verifies the counterparty is in FLUSHING, it must also move to FLUSHING unless all in-flight packets are already flushed on its end, in which case it must move directly to FLUSHCOMPLETE. The initiator will also store the counterparty timeout to ensure it does not move to FLUSHCOMPLETE after the counterparty timeout has passed.
  • The counterparty chain must prove that the initiator is also in FLUSHING or completed flushing in FLUSHCOMPLETE. The counterparty will store the initiator timeout to ensure it does not move to FLUSHCOMPLETE after the initiator timeout has passed.
FLUSHING is a “blocking” state that prevents a channel end from advancing to FLUSHCOMPLETE unless the in-flight packets on its channel end are flushed and both channel ends have already moved to FLUSHING. Once both sides have moved to FLUSHCOMPLETE, a relayer can prove this on both ends with ChanUpgradeOpen to open the channel on both sides with the new parameters.

ChannelEnd

interface ChannelEnd {
  state: ChannelState
  ordering: ChannelOrder
  counterpartyPortIdentifier: Identifier
  counterpartyChannelIdentifier: Identifier
  connectionHops: [Identifier]
  version: string
  upgradeSequence: uint64
}
  • state: The state is specified by the handshake steps of the upgrade protocol and will be mutated in place during the handshake. It will be in FLUSHING mode when the channel end is flushing in-flight packets. The state will change to FLUSHCOMPLETE once there are no in-flight packets left and the channelEnd is ready to move to OPEN.
  • upgradeSequence: The upgrade sequence will be incremented and agreed upon during the upgrade handshake and will be mutated in place.
All other parameters will remain the same during the upgrade handshake until the upgrade handshake completes. When the channel is reset to OPEN on a successful upgrade handshake, the fields on the channel end will be switched over to the UpgradeFields specified in the Upgrade.

UpgradeFields

interface UpgradeFields {
  version: string
  ordering: ChannelOrder
  connectionHops: [Identifier]
}
MAY BE MODIFIED:
  • version: The version MAY be modified by the upgrade protocol. The same version negotiation that happens in the initial channel handshake can be employed for the upgrade handshake.
  • ordering: The ordering MAY be modified by the upgrade protocol so long as the new ordering is supported by underlying connection.
  • connectionHops: The connectionHops MAY be modified by the upgrade protocol.
MUST NOT BE MODIFIED:
  • counterpartyChannelIdentifier: The counterparty channel identifier MUST NOT be modified by the upgrade protocol.
  • counterpartyPortIdentifier: The counterparty port identifier MUST NOT be modified by the upgrade protocol
NOTE: If the upgrade adds any fields to the ChannelEnd these are by default modifiable, and can be arbitrarily chosen by an Actor (e.g. chain governance) which has permission to initiate the upgrade.

Timeout

interface Timeout {
  timeoutHeight: Height
  timeoutTimestamp: uint64
}
  • timeoutHeight: Timeout height indicates the height at which the counterparty must no longer proceed with the upgrade handshake. The chains will then preserve their original channel and the upgrade handshake is aborted.
  • timeoutTimestamp: Timeout timestamp indicates the time on the counterparty at which the counterparty must no longer proceed with the upgrade handshake. The chains will then preserve their original channel and the upgrade handshake is aborted.
At least one of the timeoutHeight or timeoutTimestamp MUST be non-zero.

Upgrade

The upgrade type will represent a particular upgrade attempt on a channel end.
interface Upgrade {
  fields: UpgradeFields
  timeout: Timeout
  nextSequenceSend: uint64
}
The upgrade contains the proposed upgrade for the channel end on the executing chain, the timeout for the upgrade attempt, and the next packet send sequence for the channel. The nextSequenceSend allows the counterparty to know which packets need to be flushed before the channel can reopen with the newly negotiated parameters. Any packet sent to the channel end with a packet sequence greater than or equal to the nextSequenceSend will be rejected until the upgrade is complete. The nextSequenceSend will also be used to set the new sequences for the counterparty when it opens for a new upgrade.

ErrorReceipt

interface ErrorReceipt {
  sequence: uint64
  errorMsg: string
}
  • sequence contains the upgradeSequence at which the error occurred.
  • errorMsg contains an arbitrary string which chains may use to provide additional information as to why the upgrade was aborted.

Store Paths

Channel Upgrade Path

The chain must store the proposed upgrade upon initiating an upgrade. The proposed upgrade must be stored in the provable store. It may be deleted once the upgrade is successful or has been aborted.
function channelUpgradePath(portIdentifier: Identifier, channelIdentifier: Identifier): Path {
  return "channelUpgrades/upgrades/ports/{portIdentifier}/channels/{channelIdentifier}"
}
The upgrade path has an associated membership verification method added to the connection interface so that a counterparty may verify that chain has stored and committed to a particular set of upgrade parameters.
// Connection VerifyChannelUpgrade method
function verifyChannelUpgrade(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  upgrade: Upgrade
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(
    connection.counterpartyPrefix, 
    channelUpgradePath(counterpartyPortIdentifier, counterpartyChannelIdentifier)
  )
  return verifyMembership(clientState, height, 0, 0, proof, path, upgrade)
}

CounterpartyUpgrade Path

The chain must store the counterparty upgrade on chanUpgradeAck and chanUpgradeConfirm. This will be stored in the counterpartyUpgrade path on the private store.
function counterpartyUpgradePath(portIdentifier: Identifier, channelIdentifier: Identifier): Path {
    return "channelUpgrades/counterpartyUpgrade/ports/{portIdentifier}/channels/{channelIdentifier}"
}

Upgrade Error Path

The upgrade error path is a public path that can signal an error of the upgrade to the counterparty for the given upgrade attempt. It does not store anything in the successful case, but it will store the ErrorReceipt in the case that a chain does not accept the proposed upgrade.
function channelUpgradeErrorPath(portIdentifier: Identifier, channelIdentifier: Identifier): Path {
    return "channelUpgrades/upgradeError/ports/{portIdentifier}/channels/{channelIdentifier}"
}
The upgrade error MUST have an associated verification membership and non-membership function added to the connection interface so that a counterparty may verify that chain has stored a non-empty error in the upgrade error path.
// Connection VerifyChannelUpgradeError method
function verifyChannelUpgradeError(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  upgradeErrorReceipt: ErrorReceipt
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(
    connection.counterpartyPrefix, 
    channelUpgradeErrorPath(counterpartyPortIdentifier, counterpartyChannelIdentifier)
  )
  return verifyMembership(clientState, height, 0, 0, proof, path, upgradeErrorReceipt)
}

Sub-Protocols

The channel upgrade process consists of the following sub-protocols: initUpgradeHandshake, startFlushUpgradeHandshake, openUpgradeHandshake, cancelChannelUpgrade, and timeoutChannelUpgrade. In the case where both chains approve of the proposed upgrade, the upgrade handshake protocol should complete successfully and the ChannelEnd should upgrade to the new parameters in OPEN state.

Utility Functions

initUpgradeHandshake is a sub-protocol that will initialize the channel end for the upgrade handshake. It will validate the upgrade parameters and store the channel upgrade. All packet processing will continue according to the original channel parameters, as this is a signalling mechanism that can remain indefinitely. The new proposed upgrade will be stored in the provable store for counterparty verification. If it is called again before the handshake starts, then the current proposed upgrade will be replaced with the new one and the channel upgrade sequence will be incremented.
// initUpgradeHandshake will verify that the channel is in the
// correct precondition to call the initUpgradeHandshake protocol.
// it will verify the new upgrade field parameters, and make the
// relevant state changes for initializing a new upgrade:
// - store channel upgrade
// - incrementing upgrade sequence
function initUpgradeHandshake(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  proposedUpgradeFields: UpgradeFields,
): uint64 {
  // current channel must be OPEN
  // If channel already has an upgrade but isn't in FLUSHING,
  // then this will override the previous upgrade attempt
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === OPEN)

  // new channel version must be nonempty
  abortTransactionUnless(proposedUpgradeFields.Version !== "")

  // proposedConnection must exist and be in OPEN state for 
  // channel upgrade to be accepted
  proposedConnection = provableStore.get(connectionPath(proposedUpgradeFields.connectionHops[0]))
  abortTransactionUnless(proposedConnection !== null && proposedConnection.state === OPEN)

  // new order must be supported by the new connection
  abortTransactionUnless(isSupported(proposedConnection, proposedUpgradeFields.ordering))

  // nextSequenceSend and timeout will be filled when we move to FLUSHING
  upgrade = Upgrade{
    fields: proposedUpgradeFields,
  }

  // store upgrade in provable store for counterparty proof verification
  provableStore.set(channelUpgradePath(portIdentifier, channelIdentifier), upgrade)

  channel.upgradeSequence = channel.upgradeSequence + 1
  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)
  return channel.upgradeSequence
}
isCompatibleUpgradeFields will return true if two upgrade field structs are mutually compatible as counterparties, and false otherwise. The first field must be the upgrade fields on the executing chain, the second field must be the counterparty upgrade fields. This function will also check that the proposed connection hops exists, is OPEN, and is mutually compatible with the counterparty connection hops.
function isCompatibleUpgradeFields(
  proposedUpgradeFields: UpgradeFields,
  counterpartyUpgradeFields: UpgradeFields,
): boolean {
  if (proposedUpgradeFields.ordering != counterpartyUpgradeFields.ordering) {
    return false
  }
  if (proposedUpgradeFields.version != counterpartyUpgradeFields.version) {
    return false
  }

  // connectionHops can change in a channel upgrade, however both sides must
  // still be each other's counterparty. Since connection hops may be provided
  // by relayer, we will abort to avoid changing state based on relayer-provided value
  // Note: If the proposed connection came from an existing upgrade, then the 
  // off-chain authority is responsible for replacing one side's upgrade fields
  // to be compatible so that the upgrade handshake can proceed
  proposedConnection = provableStore.get(connectionPath(proposedUpgradeFields.connectionHops[0]))
  if (proposedConnection == null || proposedConnection.state != OPEN) {
    return false
  }
  if (counterpartyUpgradeFields.connectionHops[0] != proposedConnection.counterpartyConnectionIdentifier) {
    return false
  }
  return true
}
startFlushUpgradeHandshake will block the upgrade from continuing until all in-flight packets have been flushed. It will set the channel state to FLUSHING and block sendPacket. During this time; receivePacket, acknowledgePacket and timeoutPacket will still be allowed and processed according to the original channel parameters. The state machine will set a timer for how long the other side can take before it completes flushing and moves to FLUSHCOMPLETE. The new proposed upgrade will be stored in the public store for counterparty verification.
// startFlushUpgradeHandshake will verify that the channel
// is in a valid precondition for calling the startFlushUpgradeHandshake.
// it will set the channel to flushing state.
// it will store the nextSequenceSend and upgrade timeout in the upgrade state.
function startFlushUpgradeHandshake(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
) {
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === OPEN)

  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  abortTransactionUnless(upgrade !== null)

  channel.state = FLUSHING

  upgradeTimeout = getUpgradeTimeout(channel.portIdentifier, channel.channelIdentifier)
  // either timeout height or timestamp must be non-zero
  abortTransactionUnless(upgradeTimeout.timeoutHeight != 0 || upgradeTimeout.timeoutTimestamp != 0)

  nextSequenceSend = provableStore.get(nextSequenceSendPath(portIdentifier, channelIdentifier))

  upgrade.timeout = upgradeTimeout
  upgrade.nextSequenceSend = nextSequenceSend
  
  // store upgrade in public store for counterparty proof verification
  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)
  provableStore.set(channelUpgradePath(portIdentifier, channelIdentifier), upgrade)
}
openUpgradeHandshake will open the channel and switch the existing channel parameters to the newly agreed-upon upgraded channel fields.
// openUpgradeHandshake will switch the channel fields 
// over to the agreed upon upgrade fields.
// it will reset the channel state to OPEN.
// it will delete auxiliary upgrade state.
// caller must do all relevant checks before calling this function.
function openUpgradeHandshake(
  portIdentifier: Identifier,
  channelIdentifier: Identifier
) {
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))

  // if channel order changed, we need to set
  // the recv and ack sequences appropriately
  if channel.order == "UNORDERED" && upgrade.fields.ordering == "ORDERED" {
    selfNextSequenceSend = provableStore.get(nextSequenceSendPath(portIdentifier, channelIdentifier))
    counterpartyUpgrade = privateStore.get(counterpartyUpgradePath(portIdentifier, channelIdentifier))

    // set nextSequenceRecv to the counterparty nextSequenceSend since all packets were flushed
    provableStore.set(nextSequenceRecvPath(portIdentifier, channelIdentifier), counterpartyUpgrade.nextSequenceSend)
    // set nextSequenceAck to our own nextSequenceSend since all packets were flushed
    provableStore.set(nextSequenceAckPath(portIdentifier, channelIdentifier), selfNextSequenceSend)
  } else if channel.order == "ORDERED" && upgrade.fields.ordering == "UNORDERED" {
    // reset recv and ack sequences to 1 for UNORDERED channel
    provableStore.set(nextSequenceRecvPath(portIdentifier, channelIdentifier), 1)
    provableStore.set(nextSequenceAckPath(portIdentifier, channelIdentifier), 1)
  }

  // switch channel fields to upgrade fields
  // and set channel state to OPEN
  channel.ordering = upgrade.fields.ordering
  channel.version = upgrade.fields.version
  channel.connectionHops = upgrade.fields.connectionHops
  channel.state = OPEN
  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)

  // IMPLEMENTATION DETAIL: Implementations may choose to prune stale acknowledgements and receipts at this stage
  // Since flushing has completed, any acknowledgement or receipt written before the chain went into flushing has
  // already been processed by the counterparty and can be removed.
  // Implementations may do this pruning work over multiple blocks for gas reasons. In this case, they should be sure
  // to only prune stale acknowledgements/receipts and not new ones that have been written after the channel has reopened.
  // Implementations may use the counterparty NextSequenceSend as a way to determine which acknowledgement/receipts
  // were already processed by counterparty when flushing completed

  // delete auxiliary state
  provableStore.delete(channelUpgradePath(portIdentifier, channelIdentifier))
  privateStore.delete(counterpartyUpgradePath(portIdentifier, channelIdentifier))
}
restoreChannel will write an ErrorReceipt, set the channel back to its original state and delete upgrade information when the executing channel needs to abort the upgrade handshake and return to the original parameters.
// restoreChannel will restore the channel state to its pre-upgrade state
// and delete upgrade auxiliary state so that upgrade is aborted.
// it writes an error receipt to state so counterparty can restore as well.
// NOTE: this function signature may be modified by implementers to take a custom error
function restoreChannel(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
) {
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  errorReceipt = ErrorReceipt{
    channel.upgradeSequence,
    "upgrade handshake is aborted", // constant string changeable by implementation
  }
  provableStore.set(channelUpgradeErrorPath(portIdentifier, channelIdentifier), errorReceipt)
  channel.state = OPEN
  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)

  // delete auxiliary state
  provableStore.delete(channelUpgradePath(portIdentifier, channelIdentifier))
  privateStore.delete(counterpartyUpgradePath(portIdentifier, channelIdentifier))
}
pendingInflightPackets will return the list of in-flight packet sequences sent from this ChannelEnd. This can be monitored since the packet commitments are deleted when the packet lifecycle is complete. Thus if the packet commitment exists on the sender chain, the packet lifecycle is incomplete. The pseudocode is not provided in this spec since it will be dependent on the state machine in-question. The ibc-go implementation will use the store iterator to implement this functionality. The function signature is provided below:
// pendingInflightPacketSequences returns the packet sequences sent on 
// this end that have not had their lifecycle completed
function pendingInflightPacketSequences(
  portIdentifier: Identifier,
  channelIdentifier: Identifier
): [uint64]
isAuthorizedUpgrader will return true if the provided address is authorized to initialize, modify, and cancel upgrades. Chains may permission a set of addresses that can signal which upgrade a channel is willing to upgrade to.
// isAuthorizedUpgrader
function isAuthorizedUpgrader(address: string): boolean
getUpgradeTimeout will return the upgrade timeout specified for the given channel. This may be a chain-wide parameter, or it can be a parameter chosen per channel. This is an implementation-level detail, so only the function signature is specified here. Note this should retrieve some stored timeout delta for the channel and add it to the current height and time to get the absolute timeout values.
// getUpgradeTimeout
function getUpgradeTimeout(portIdentifier: string, channelIdentifier: string) Timeout {
}

Upgrade Handshake

The upgrade handshake defines seven datagrams: ChanUpgradeInit, ChanUpgradeTry, ChanUpgradeAck, ChanUpgradeConfirm, ChanUpgradeOpen, ChanUpgradeTimeout, and ChanUpgradeCancel A successful protocol execution flows as follows (note that all calls are made through modules per ICS 25):
InitiatorDatagramChain acted uponPrior state (A, B)Posterior state (A, B)
ActorChanUpgradeInitA(OPEN, OPEN)(OPEN, OPEN)
RelayerChanUpgradeTryB(OPEN, OPEN)(OPEN, FLUSHING)
RelayerChanUpgradeAckA(OPEN, FLUSHING)(FLUSHING/FLUSHCOMPLETE, FLUSHING)
RelayerChanUpgradeConfirmB(FLUSHING/FLUSHCOMPLETE, FLUSHING)(FLUSHING/FLUSHCOMPLETE, FLUSHING/FLUSHCOMPLETE/OPEN)
IMPORTANT: Note it is important that the prior state before the channel upgrade process starts is that both channel ends are OPEN. Authorized upgraders are at risk of having the channel halt during the upgrade process if the prior state before channel upgrades on one of the ends is not OPEN. Refer to the diagram below for a possible channel upgrade flow. Multiple channel states are shown on steps 5 and 7 where the channel end can move to either one of those possible states upon executing the handshake. Note that in this example, the channel end on chain B moves to OPEN with the new parameters on ChanUpgradeConfirm (step 7). Channel Upgrade Flow Once both states are in FLUSHING and both sides have stored each others upgrade timeouts, both sides can move to FLUSHCOMPLETE by clearing their in-flight packets. Once both sides have complete flushing, a relayer may submit a ChanUpgradeOpen datagram to both ends proving that the counterparty has also completed flushing in order to move the channelEnd to OPEN. ChanUpgradeOpen is only necessary to call on chain B if the chain was not moved to OPEN on ChanUpgradeConfirm which may happen if all packets on both ends are already flushed. At the end of a successful upgrade handshake between two chains implementing the sub-protocol, the following properties hold:
  • Each chain is running their new upgraded channel end and is processing upgraded logic and state according to the upgraded parameters.
  • Each chain has knowledge of and has agreed to the counterparty’s upgraded channel parameters.
  • All packets sent before the handshake have been completely flushed (acked or timed out) with the old parameters.
  • All packets sent after a channel end moves to OPEN will either timeout using new parameters on sending channelEnd or will be received by the counterparty using new parameters.
If a chain does not agree to the proposed counterparty upgraded ChannelEnd, it may abort the upgrade handshake by writing an ErrorReceipt into the channelUpgradeErrorPath and restoring the original channel. The ErrorReceipt must contain the current upgrade sequence on the erroring chain’s channel end. channelUpgradeErrorPath(portID, channelID) => ErrorReceipt(sequence, msg) A relayer may then submit a ChanUpgradeCancel datagram to the counterparty. Upon receiving this message a chain must verify that the counterparty wrote an ErrorReceipt into its channelUpgradeErrorPath with a sequence greater than or equal to its own ChannelEnd’s upgrade sequence. If successful, it will restore its original channel as well, thus cancelling the upgrade. If a chain does not reach FLUSHCOMPLETE within the counterparty specified timeout, then it MUST NOT move to FLUSHCOMPLETE and should instead abort the upgrade. A relayer may submit a proof of this to the counterparty chain in a ChanUpgradeTimeout datagram so that counterparty cancels the upgrade and restores its original channel as well.
// Channel Ends on both sides **must** be OPEN before this function is called
// It is the responsibility of the authorized upgrader to ensure this is the case
function chanUpgradeInit(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  proposedUpgradeFields: UpgradeFields,
  msgSender: string,
) {
  // chanUpgradeInit may only be called by addresses authorized by executing chain
  abortTransactionUnless(isAuthorizedUpgrader(msgSender))

  // if a previous upgrade attempt exists, then delete it and write error receipt, so
  // counterparty can abort it and move to next upgrade
  existingUpgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  if existingUpgrade != null {
    provableStore.delete(channelUpgradePath(portIdentifier, channelIdentifier))
    errorReceipt = ErrorReceipt{
      channel.upgradeSequence,
      "abort the previous upgrade attempt so counterparty can accept the new one", // constant string changeable by implementation
    }
    provableStore.set(channelUpgradeErrorPath(portIdentifier, channelIdentifier), errorReceipt)
  }

  upgradeSequence = initUpgradeHandshake(portIdentifier, channelIdentifier, proposedUpgradeFields)

  // call modules onChanUpgradeInit callback
  // onChanUpgradeInit may return a new proposed version
  // if an error is returned the upgrade is not written
  // the callback MUST NOT write state, as all state transitions will occur once
  // the channel upgrade is complete.
  module = lookupModule(portIdentifier)
  version, err = module.onChanUpgradeInit(
    portIdentifier,
    channelIdentifier,
    upgradeSequence,
    proposedUpgradeFields.ordering,
    proposedUpgradeFields.connectionHops,
    proposedUpgradeFields.version
  )
  // abort transaction if callback returned error
  abortTransactionUnless(err === null)

  // replace channel upgrade version with the version returned by application
  // in case it was modified
  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  upgrade.fields.version = version
  provableStore.set(channelUpgradePath(portIdentifier, channelIdentifier), upgrade)
}
NOTE: It is up to individual implementations how they will provide access-control to the chanUpgradeInit function. E.g. chain governance, permissioned actor, DAO, etc.
function chanUpgradeTry(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyUpgrade: Upgrade,
  counterpartyUpgradeSequence: uint64,
  proposedConnectionHops: [Identifier],
  proofChannel: CommitmentProof,
  proofUpgrade: CommitmentProof,
  proofHeight: Height
) {
  // current channel must be OPEN (i.e. not in FLUSHING)
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === OPEN)

  // construct counterpartyChannel from existing information and provided
  // counterpartyUpgradeSequence
  counterpartyChannel = ChannelEnd{
    state: OPEN,
    ordering: channel.ordering,
    counterpartyPortIdentifier: portIdentifier,
    counterpartyChannelIdentifier: channelIdentifier,
    connectionHops: counterpartyHops,
    version: channel.version,
    sequence: counterpartyUpgradeSequence,
  }

  // verify proofs of counterparty state
  abortTransactionUnless(
    verifyChannelState(
      connection,
      proofHeight,
      proofChannel,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyChannel
    )
  )
  abortTransactionUnless(
    verifyChannelUpgrade(
      connection,
      proofHeight,
      proofUpgrade,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyUpgrade
    )
  )

  existingUpgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  if existingUpgrade != null {
    expectedUpgradeSequence = channel.UpgradeSequence
  } else {
    // at the end of the TRY step, the current upgrade sequence will be incremented in the non-crossing
    // hello case due to calling chanUpgradeInit, we should use this expected upgrade sequence for
    // sequence mismatch comparison
    expectedUpgradeSequence = channel.UpgradeSequence + 1
  }

  // NON CROSSING HELLO CASE:
  // if the counterparty sequence is less than or equal to the current sequence,
  // then either the counterparty chain is out-of-sync or the message
  // is out-of-sync and we write an error receipt with our sequence
  // so that the counterparty can abort their attempt and resync with our sequence.
  // When the next upgrade attempt is initiated, both sides will move to a fresh
  // never-before-seen sequence number
  // CROSSING HELLO CASE:
  // if the counterparty sequence is less than the current sequence,
  // then either the counterparty chain is out-of-sync or the message
  // is out-of-sync and we write an error receipt with our sequence minus one
  // so that the counterparty can update their sequence as well.
  // This will cause the outdated counterparty to upgrade the sequence
  // and abort their out-of-sync upgrade without aborting our own since
  // the error receipt sequence is lower than ours and higher than the counterparty.
  if counterpartyUpgradeSequence < expectedUpgradeSequence {
    errorReceipt = ErrorReceipt{
      expectedUpgradeSequence - 1,
      "sequence out of sync", // constant string changeable by implementation
    }
    provableStore.set(channelUpgradeErrorPath(portIdentifier, channelIdentifier), errorReceipt)
    return
  }
  
  // create upgrade fields for this chain from counterparty upgrade and 
  // relayer-provided information version may be mutated by application callback
  upgradeFields = Upgrade{
    ordering: counterpartyUpgrade.fields.ordering,
    connectionHops: proposedConnectionHops,
    version: counterpartyUpgrade.fields.version,
  }

  // current upgrade either doesn't exist (non-crossing hello case),
  // we initialize the upgrade with constructed upgradeFields
  // if it does exist, we are in crossing hellos and must assert
  // that the upgrade fields are the same for crossing-hellos case
  if (existingUpgrade == null) {
    initUpgradeHandshake(portIdentifier, channelIdentifier, upgradeFields)
  } else {
    // we must use the existing upgrade fields
    upgradeFields = existingUpgrade.fields
  }

  abortTransactionUnless(isCompatibleUpgradeFields(upgradeFields, counterpartyUpgradeFields))

  // if the counterparty sequence is greater than the current sequence,
  // we fast forward to the counterparty sequence so that both channel 
  // ends are using the same sequence for the current upgrade.
  // initUpgradeHandshake will increment the sequence so after that call
  // both sides will have the same upgradeSequence
  if (counterpartyUpgradeSequence > channel.upgradeSequence) {
    channel.upgradeSequence = counterpartyUpgradeSequence
  }
  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)

  // get counterpartyHops for given connection
  connection = provableStore.get(connectionPath(channel.connectionHops[0]))
  counterpartyHops = [connection.counterpartyConnectionIdentifier]

  // call startFlushUpgradeHandshake to move channel to FLUSHING, which will block
  // upgrade from progressing to OPEN until flush completes on both ends
  startFlushUpgradeHandshake(portIdentifier, channelIdentifier)

  // refresh channel to get latest state
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))

  // call modules onChanUpgradeTry callback
  // onChanUpgradeTry may return a new proposed version
  // if an error is returned the upgrade is not written
  // the callback MUST NOT write state, as all state transitions will occur once
  // the channel upgrade is complete.
  module = lookupModule(portIdentifier)
  version, err = module.onChanUpgradeTry(
    portIdentifier,
    channelIdentifier,
    channel.upgradeSequence,
    upgradeFields.ordering,
    upgradeFields.connectionHops,
    upgradeFields.version
  )
  // abort the transaction if the callback returns an error and
  // there was no existing upgrade. This will allow the counterparty upgrade
  // to continue existing while this chain may add support for it in the future
  abortTransactionUnless(err === null)

  // replace channel version with the version returned by application
  // in case it was modified
  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  upgrade.fields.version = version
  provableStore.set(channelUpgradePath(portIdentifier, channelIdentifier), upgrade)
}
NOTE: Implementations that want to explicitly permission upgrades should enforce crossing hellos. i.e. Both parties must have called ChanUpgradeInit with mutually compatible parameters in order for ChanUpgradeTry to succeed. Implementations that want to be permissive towards counterparty-initiated upgrades may allow moving from OPEN to FLUSHING without having an upgrade previously stored on the executing chain.
function chanUpgradeAck(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyUpgrade: Upgrade,
  proofChannel: CommitmentProof,
  proofUpgrade: CommitmentProof,
  proofHeight: Height
) {
  // current channel is OPEN or FLUSHING (crossing hellos)
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state == OPEN || channel.state == FLUSHING)

  connection = provableStore.get(connectionPath(channel.connectionHops[0]))
  counterpartyHops = [connection.counterpartyConnectionIdentifier]

  // construct counterpartyChannel from existing information
  counterpartyChannel = ChannelEnd{
    state: FLUSHING,
    ordering: channel.ordering,
    counterpartyPortIdentifier: portIdentifier,
    counterpartyChannelIdentifier: channelIdentifier,
    connectionHops: counterpartyHops,
    version: channel.version,
    sequence: channel.upgradeSequence,
  }

  // verify proofs of counterparty state
  abortTransactionUnless(
    verifyChannelState(
      connection,
      proofHeight,
      proofChannel,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyChannel
    )
  )
  abortTransactionUnless(
    verifyChannelUpgrade(
      connection,
      proofHeight,
      proofUpgrade,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyUpgrade
    )
  )

  existingUpgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))

  // optimistically accept version that TRY chain proposes and pass this to callback for confirmation.
  // in the crossing hello case, we do not modify version that our TRY call returned and instead 
  // enforce that both TRY calls returned the same version
  if (channel.state == OPEN) {
    existingUpgrade.fields.version == counterpartyUpgrade.fields.version
  }
  // if upgrades are not compatible by ACK step, then we restore the channel
  if (!isCompatibleUpgradeFields(existingUpgrade.fields, counterpartyUpgrade.fields)) {
    restoreChannel(portIdentifier, channelIdentifier)
    return
  }

  if (channel.state == OPEN) {
    // prove counterparty and move our own state to flushing
    // if we are already at flushing, then no state changes occur
    // upgrade is blocked on this channelEnd from progressing until flush completes on its end
    startFlushUpgradeHandshake(portIdentifier, channelIdentifier)
    // startFlushUpgradeHandshake sets the timeout for the upgrade
    // so retrieve upgrade again here and use that timeout value
    upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
    existingUpgrade.timeout = upgrade.timeout
  }

  timeout = counterpartyUpgrade.timeout
  
  // counterparty-specified timeout must not have exceeded
  // if it has, then restore the channel and abort upgrade handshake
  if ((timeout.timeoutHeight != 0 && currentHeight() >= timeout.timeoutHeight) ||
      (timeout.timeoutTimestamp != 0 && currentTimestamp() >= timeout.timeoutTimestamp )) {
        restoreChannel(portIdentifier, channelIdentifier)
        return
  }

  // if there are no in-flight packets on our end, we can automatically go to FLUSHCOMPLETE
  if (pendingInflightPackets(portIdentifier, channelIdentifier) == null) {
    channel.state = FLUSHCOMPLETE
  }
  // set counterparty upgrade
  privateStore.set(counterpartyUpgradePath(portIdentifier, channelIdentifier), counterpartyUpgrade)

  provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)

  // call modules onChanUpgradeAck callback
  // module can error on counterparty version
  // ACK should not change state to the new parameters yet
  // as that will happen on the onChanUpgradeOpen callback
  module = lookupModule(portIdentifier)
  err = module.onChanUpgradeAck(
    portIdentifier,
    channelIdentifier,
    counterpartyUpgrade.fields.version
  )
  // restore channel if callback returned error
  if (err != null) {
    restoreChannel(portIdentifier, channelIdentifier)
    return
  }

  // if no error, agree on final version
  provableStore.set(channelUpgradePath(portIdentifier, channelIdentifier), existingUpgrade)
}
chanUpgradeConfirm is called on the chain which is on FLUSHING after chanUpgradeAck is called on the counterparty. This will inform the TRY chain of the timeout set on ACK by the counterparty. If the timeout has already exceeded, we will write an error receipt and restore. If packets on both sides have already been flushed and timeout is not exceeded, then we can open the channel. Otherwise, we set the counterparty timeout in the private store and wait for packet flushing to complete.
function chanUpgradeConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannelState: state,
  counterpartyUpgrade: Upgrade,
  proofChannel: CommitmentProof,
  proofUpgrade: CommitmentProof,
  proofHeight: Height,
) {
  // current channel is in FLUSHING
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === FLUSHING)

  // counterparty channel is either FLUSHING or FLUSHCOMPLETE
  abortTransactionUnless(counterpartyChannelState === FLUSHING || counterpartyChannelState === FLUSHCOMPLETE)

  connection = provableStore.get(connectionPath(channel.connectionHops[0]))
  counterpartyHops = [connection.counterpartyConnectionIdentifier]

  counterpartyChannel = ChannelEnd{
    state: counterpartyChannelState,
    ordering: channel.ordering,
    counterpartyPortIdentifier: portIdentifier,
    counterpartyChannelIdentifier: channelIdentifier,
    connectionHops: counterpartyHops,
    version: channel.version,
    sequence: channel.upgradeSequence,
  }

  // verify proofs of counterparty state
  abortTransactionUnless(
    verifyChannelState(
      connection,
      proofHeight,
      proofChannel,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyChannel
    )
  )
  abortTransactionUnless(
    verifyChannelUpgrade(
      connection,
      proofHeight,
      proofUpgrade, 
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyUpgrade
    )
  )

  existingUpgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))

	// in the crossing-hello case it is possible that both chains execute the
	// INIT, TRY and CONFIRM steps without any of them executing ACK, therefore
	// we also need to check that the upgrades are compatible on this step
  if (!isCompatibleUpgradeFields(existingUpgrade.fields, counterpartyUpgrade.fields)) {
    restoreChannel(portIdentifier, channelIdentifier)
    return
  }

  timeout = counterpartyUpgrade.timeout
  
  // counterparty-specified timeout must not have exceeded
  // if it has, then restore the channel and abort upgrade handshake
  if ((timeout.timeoutHeight != 0 && currentHeight() >= timeout.timeoutHeight) ||
      (timeout.timeoutTimestamp != 0 && currentTimestamp() >= timeout.timeoutTimestamp)) {
        restoreChannel(portIdentifier, channelIdentifier)
        return
  }

  // if there are no in-flight packets on our end, we can automatically go to FLUSHCOMPLETE
  if (pendingInflightPackets(portIdentifier, channelIdentifier) == null) {
    channel.state = FLUSHCOMPLETE
    provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)
  }
  // set counterparty upgrade
  privateStore.set(counterpartyUpgradePath(portIdentifier, channelIdentifier), counterpartyUpgrade)

  // if both chains are already in flushcomplete we can move to OPEN
  if (channel.state == FLUSHCOMPLETE && counterpartyChannelState == FLUSHCOMPLETE) {
    openUpgradeHandshake(portIdentifier, channelIdentifier)
    // make application state changes based on new channel parameters
    module.onChanUpgradeOpen(portIdentifier, channelIdentifier)
  }
}
chanUpgradeOpen may only be called once both sides have moved to FLUSHCOMPLETE. If there exists unprocessed packets in the queue when the handshake goes into FLUSHING mode, then the packet handlers must move the channel end to FLUSHCOMPLETE once the last packet on the channel end has been processed.
function chanUpgradeOpen(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannelState: ChannelState,
  counterpartyUpgradeSequence: uint64,
  proofChannel: CommitmentProof,
  proofHeight: Height,
) {
  // channel must have completed flushing
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === FLUSHCOMPLETE)

  // get connection for proof verification
  connection = provableStore.get(connectionPath(channel.connectionHops[0]))

  // counterparty must be in OPEN or FLUSHCOMPLETE state
  if (counterpartyChannelState == OPEN) {
    // get upgrade since counterparty should have upgraded to these parameters
    upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))

    // get the counterparty's connection hops for the proposed upgrade connection
    proposedConnection = provableStore.get(connectionPath(upgrade.fields.connectionHops))
    counterpartyHops = [proposedConnection.counterpartyConnectionIdentifier]

    // The counterparty upgrade sequence must be greater than or equal to
    // the channel upgrade sequence. It should normally be equivalent, but
    // in the unlikely case that a new upgrade is initiated after it reopens,
    // then the upgrade sequence will be greater than our upgrade sequence.
    abortTransactionUnless(counterpartyUpgradeSequence >= channel.upgradeSequence)

    counterpartyChannel = ChannelEnd{
      state: OPEN,
      ordering: upgrade.fields.ordering,
      counterpartyPortIdentifier: portIdentifier,
      counterpartyChannelIdentifier: channelIdentifier,
      connectionHops: counterpartyHops,
      version: upgrade.fields.version,
      sequence: counterpartyUpgradeSequence,
    }
  } else if (counterpartyChannelState == FLUSHCOMPLETE) {
    counterpartyHops = [connection.counterpartyConnectionIdentifier]
    counterpartyChannel = ChannelEnd{
      state: FLUSHCOMPLETE,
      ordering: channel.ordering,
      counterpartyPortIdentifier: portIdentifier,
      counterpartyChannelIdentifier: channelIdentifier,
      connectionHops: counterpartyHops,
      version: channel.version,
      sequence: channel.upgradeSequence,
    }
  } else {
    abortTransactionUnless(false)
  }

  abortTransactionUnless(
    verifyChannelState(
      connection, 
      proofHeight, 
      proofChannel, 
      channel.counterpartyPortIdentifier, 
      channel.counterpartyChannelIdentifier, 
      counterpartyChannel
    )
  )

  // move channel to OPEN and adopt upgrade parameters
  openUpgradeHandshake(portIdentifier, channelIdentifier)

  // call modules onChanUpgradeOpen callback
  module = lookupModule(portIdentifier)
  // open callback must not return error since counterparty successfully upgraded
  // make application state changes based on new channel parameters
  module.onChanUpgradeOpen(
    portIdentifier,
    channelIdentifier
  )
}

Cancel Upgrade Process

During the upgrade handshake a chain may cancel the upgrade by writing an error receipt into the upgrade error path and restoring the original channel to OPEN. The counterparty must then restore its channel to OPEN as well. A relayer can facilitate this by sending ChanUpgradeCancel datagram to the handler:
function cancelChannelUpgrade(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  errorReceipt: ErrorReceipt,
  proofUpgradeError: CommitmentProof,
  proofHeight: Height,
  msgSender: string,
) {
  // current channel has an upgrade stored
  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  abortTransactionUnless(upgrade !== null)

  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  // if the msgSender is authorized to make and cancel upgrades AND 
  // the current channel has not already reached FLUSHCOMPLETE,
  // then we can restore immediately without any additional checks
  // otherwise, we can only cancel if the counterparty wrote an
  // error receipt during the upgrade handshake
  if (!(isAuthorizedUpgrader(msgSender) && channel.state != FLUSHCOMPLETE)) {
    abortTransactionUnless(!isEmpty(errorReceipt))

    if channel.state == FLUSHCOMPLETE {
      // if the channel state is in FLUSHCOMPLETE, it can **only** be aborted if there
      // is an error receipt with the exact same sequence. This ensures that the counterparty
      // did not successfully upgrade and then cancel at a new upgrade to abort our own end,
      // leading to both channel ends being OPEN with different parameters
      abortTransactionUnless(errorReceipt.sequence == channel.upgradeSequence)
    } else {
      // If counterparty sequence is less than the current sequence,
      // abort transaction since this error receipt is from a previous upgrade
      abortTransactionUnless(errorReceipt.sequence >= channel.upgradeSequence)
    }
    // fastforward channel sequence to higher sequence so that we can start
    // new handshake on a fresh sequence
    channel.upgradeSequence = errorReceipt.sequence
    provableStore.set(channelPath(portIdentifier, channelIdentifier), channel)

    // get underlying connection for proof verification
    connection = provableStore.get(connectionPath(channel.connectionHops[0]))
    // verify that the provided error receipt is written to the upgradeError path with the counterparty sequence
    abortTransactionUnless(
      verifyChannelUpgradeError(
        connection,
        proofHeight,
        proofUpgradeError,
        channel.counterpartyPortIdentifier,
        channel.counterpartyChannelIdentifier,
        errorReceipt
      )
    )
  }

  // cancel upgrade and write error receipt
  restoreChannel(portIdentifier, channelIdentifier)
}

Timeout Upgrade Process

It is possible for the channel upgrade process to stall indefinitely while trying to flush the existing packets. To protect against this, each chain sets a timeout when it moves into FLUSHING. If the counterparty has not completed flushing within the expected time window, then the relayer can submit a timeout message to restore the channel to OPEN with the original parameters. It will also write an error receipt so that the counterparty which has not moved to FLUSHCOMPLETE can also restore channel to OPEN with the original parameters.
function timeoutChannelUpgrade(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannel: ChannelEnd,
  proofChannel: CommitmentProof,
  proofHeight: Height,
) {
  // current channel must have an upgrade that is FLUSHING or FLUSHCOMPLETE
  upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))
  abortTransactionUnless(upgrade !== null)
  channel = provableStore.get(channelPath(portIdentifier, channelIdentifier))
  abortTransactionUnless(channel.state === FLUSHING || channel.state === FLUSHCOMPLETE)

  upgradeTimeout = upgrade.timeout

  // proof must be from a height after timeout has elapsed. 
  // Either timeoutHeight or timeoutTimestamp must be defined.
  // if timeoutHeight is defined and proof is from before 
  // timeout height then abort transaction
  abortTransactionUnless(
    upgradeTimeout.timeoutHeight.IsZero() || 
    proofHeight >= upgradeTimeout.timeoutHeight
  )
  // if timeoutTimestamp is defined then the consensus time 
  // from proof height must be greater than timeout timestamp
  connection = provableStore.get(connectionPath(channel.connectionHops[0]))
  abortTransactionUnless(
    upgradeTimeout.timeoutTimestamp.IsZero() || 
    getTimestampAtHeight(connection, proofHeight) >= upgradeTimeout.timestamp
  )

  // counterparty channel must be proved to not have completed flushing after timeout has passed
  abortTransactionUnless(counterpartyChannel.state !== FLUSHCOMPLETE)
  // if counterparty channel state is OPEN, we should abort the tx
  // only if the counterparty has successfully completed upgrade
  if (counterpartyChannel.state == OPEN) {
    // get upgrade since counterparty should have upgraded to these parameters
    upgrade = provableStore.get(channelUpgradePath(portIdentifier, channelIdentifier))

    // get counterparty hops of the proposed connection
    proposedConnection = provableStore.get(connectionPath(upgrade.fields.connectionHops))
    counterpartyHops = [proposedConnection.counterpartyConnectionIdentifier]

    // check that the channel did not upgrade successfully
    if ((upgrade.fields.version == counterpartyChannel.version) &&
        (upgrade.fields.order == counterpartyChannel.order) &&
        (counterpartyHops == counterpartyChannel.connectionHops)) {
          // counterparty has already successfully upgraded so we cannot timeout
          abortTransactionUnless(false)
    }
  }
  abortTransactionUnless(counterpartyChannel.upgradeSequence >= channel.upgradeSequence)
  abortTransactionUnless(
    verifyChannelState(
      connection,
      proofHeight,
      proofChannel,
      channel.counterpartyPortIdentifier,
      channel.counterpartyChannelIdentifier,
      counterpartyChannel
    )
  )

  // we must restore the channel since the timeout verification has passed
  // error receipt is written for this sequence, counterparty can call cancelUpgradeHandshake
  restoreChannel(portIdentifier, channelIdentifier)
}
Both parties must not complete the upgrade handshake and move to FLUSHCOMPLETE if the counterparty upgrade timeout has already passed. This will prevent the channel ends from reaching incompatible states.

Considerations

Note that a channel upgrade handshake may never complete successfully if the in-flight packets cannot successfully be cleared. This can happen if the timeout value of a packet is too large, or an acknowledgement never arrives, or if there is a bug that makes acknowledging or timing out a packet impossible. In these cases, some out-of-protocol mechanism (e.g. governance) must step in to clear the packets “manually” perhaps by forcefully clearing the packet commitments before restarting the upgrade handshake.

Migrations

A chain may have to update its internal state to be consistent with the new upgraded channel. In this case, a migration handler should be a part of the chain binary before the upgrade process so that the chain can properly migrate its state once the upgrade is successful. If a migration handler is necessary for a given upgrade but is not available, then the executing chain must reject the upgrade so as not to enter into an invalid state. This state migration will not be verified by the counterparty since it will just assume that if the channel is upgraded to a particular channel version, then the auxiliary state on the counterparty will also be updated to match the specification for the given channel version. The migration must only run once the upgrade has successfully completed and the new channel is OPEN (ie. on ChanUpgradeConfirm or ChanUpgradeOpen).

Example Implementations

History

Feb 1, 2024 - Spec as implemented in ibc-go Jul 24, 2024 - Add upgrade compatibility check in chanUpgradeConfirm All content herein is licensed under Apache 2.0.