Upgrading Channels
Upgrading Channels
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
ChanUpgradeTrythat accepts the upgrade should store the channel upgrade, set the channel state fromOPENtoFLUSHING, and start the flushing timer by storing an upgrade timeout. - Once the initiating chain verifies the counterparty is in
FLUSHING, it must also move toFLUSHINGunless all in-flight packets are already flushed on its end, in which case it must move directly toFLUSHCOMPLETE. The initiator will also store the counterparty timeout to ensure it does not move toFLUSHCOMPLETEafter the counterparty timeout has passed. - The counterparty chain must prove that the initiator is also in
FLUSHINGor completed flushing inFLUSHCOMPLETE. The counterparty will store the initiator timeout to ensure it does not move toFLUSHCOMPLETEafter 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 inFLUSHINGmode when the channel end is flushing in-flight packets. The state will change toFLUSHCOMPLETEonce there are no in-flight packets left and the channelEnd is ready to move toOPEN.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
}
sequencecontains theupgradeSequenceat which the error occurred.errorMsgcontains 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
Excerpt (19996 of 55376 characters). Read the whole page on cosmos/ibc (ICS specifications) ↗