Skip to content

Commit db66af8

Browse files
authored
Add upgrade sequences to avoid proof collisions (#724)
* add sequences to error path * add sequence verification for all handshake methods to ensure only relevant counterparty state is used * simplify sequence logic * address pr comments * increment seq on error * fix timeout logic
1 parent 5a63430 commit db66af8

File tree

1 file changed

+131
-18
lines changed
  • spec/core/ics-004-channel-and-packet-semantics

1 file changed

+131
-18
lines changed

spec/core/ics-004-channel-and-packet-semantics/UPGRADES.md

+131-18
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,46 @@ interface UpgradeTimeout {
7575

7676
At least one of the timeoutHeight or timeoutTimestamp MUST be non-zero.
7777

78+
```typescript
79+
interface ErrorReceipt {
80+
sequence: uint64
81+
errorMsg: string
82+
}
83+
```
84+
85+
- `sequence`: Sequence contains the sequence at which the error occurred. Both chains are expected to increment to the next sequence after the upgrade is aborted.
86+
- `errorMsg`: ErrorMsg contains an arbitrary string which chains may use to provide additional information as to why the upgrade was aborted.
87+
7888
### Store Paths
7989

90+
#### UpgradeSequencePath
91+
92+
The upgrade sequence path is a public path that stores the current sequence of the upgrade attempt. The sequence will increment with each attempted upgrade on the given channel. The sequence will be used to ensure that different error receipts referring to different upgrade attempts do not interfere with each other.
93+
94+
```typescript
95+
function upgradeSequencePath(portIdentifier: Identifier, channelIdentifier: Identifier) Path {
96+
return "channelUpgrade/ports/{portIdentifier}/channelIdentifier/{channelIdentifier}/upgradeSequence"
97+
}
98+
```
99+
100+
The upgrade sequence MUST also have a verification method so that chains can prove the upgrade sequence on the counterparty for the given channel upgrade.
101+
102+
```typescript
103+
// Connection VerifyChannelUpgradeSequence method
104+
function verifyChannelUpgradeSequence(
105+
connection: ConnectionEnd,
106+
height: Height,
107+
proof: CommitmentProof,
108+
counterpartyPortIdentifier: Identifier,
109+
counterpartyChannelIdentifier: Identifier,
110+
sequence: uint64
111+
) {
112+
client = queryClient(connection.clientIdentifier)
113+
path = applyPrefix(connection.counterpartyPrefix, upgradeSequencePath(counterpartyPortIdentifier, counterpartyChannelIdentifier))
114+
client.verifyMembership(height, 0, 0, proof, path, sequence)
115+
}
116+
```
117+
80118
#### Restore Channel Path
81119

82120
The chain must store the previous channel end so that it may restore it if the upgrade handshake fails. This may be stored in the private store.
@@ -89,16 +127,15 @@ function restorePath(portIdentifier: Identifier, channelIdentifier: Identifier):
89127

90128
#### UpgradeError Path
91129

92-
The upgrade error path is a public path that can signal an error of the upgrade to the counterparty. It does not store anything in the successful case, but it will store a sentinel abort value in the case that a chain does not accept the proposed upgrade.
130+
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.
93131

94132
```typescript
95133
function errorPath(portIdentifier: Identifier, channelIdentifier: Identifier): Path {
96134
return "channelUpgrade/ports/{portIdentifier}/channels/{channelIdentifier}/upgradeError"
97-
98135
}
99136
```
100137

101-
The UpgradeError MUST have an associated verification membership and nonmembership function added to the connection interface so that a counterparty may verify that chain has stored an error in the UpgradeError path.
138+
The UpgradeError MUST have an associated verification membership and nonmembership function added to the connection interface so that a counterparty may verify that chain has stored a non-empty error in the UpgradeError path.
102139

103140
```typescript
104141
// Connection VerifyChannelUpgradeError method
@@ -108,7 +145,7 @@ function verifyChannelUpgradeError(
108145
proof: CommitmentProof,
109146
counterpartyPortIdentifier: Identifier,
110147
counterpartyChannelIdentifier: Identifier,
111-
upgradeErrorReceipt: []byte,
148+
upgradeErrorReceipt: ErrorReceipt
112149
) {
113150
client = queryClient(connection.clientIdentifier)
114151
path = applyPrefix(connection.counterpartyPrefix, channelErrorPath(counterpartyPortIdentifier, counterpartyChannelIdentifier))
@@ -170,9 +207,13 @@ The Channel Upgrade process consists of three sub-protocols: `UpgradeChannelHand
170207
```typescript
171208
function restoreChannel() {
172209
// cancel upgrade
173-
// write an error receipt into the error path
210+
// write an error receipt with the current sequence into the error path
174211
// and restore original channel
175-
errorReceipt = []byte{1}
212+
sequence = provableStore.get(upgradeSequencePath(portIdentifier, channelIdentifier))
213+
errorReceipt = ErrorReceipt{
214+
Sequence: sequence,
215+
ErrorMsg: ""
216+
}
176217
provableStore.set(errorPath(portIdentifier, channelIdentifier), errorReceipt)
177218
originalChannel = privateStore.get(restorePath(portIdentifier, channelIdentifier))
178219
provableStore.set(channelPath(portIdentifier, channelIdentifier), originalChannel)
@@ -187,6 +228,10 @@ function restoreChannel() {
187228
portIdentifier,
188229
channelIdentifier
189230
)
231+
232+
// increment sequence in preparation for the next upgrade
233+
provableStore.set(upgradeSequencePath(portIdentifier, channelIdentifier), sequence+1)
234+
190235
// caller should return as well
191236
}
192237
```
@@ -209,11 +254,11 @@ At the end of an upgrade handshake between two chains implementing the sub-proto
209254
- Each chain is running their new upgraded channel end and is processing upgraded logic and state according to the upgraded parameters.
210255
- Each chain has knowledge of and has agreed to the counterparty's upgraded channel parameters.
211256

212-
If a chain does not agree to the proposed counterparty `UpgradedChannel`, it may abort the upgrade handshake by writing an error receipt into the `errorPath` and restoring the original channel. The error receipt MAY be arbitrary bytes and MUST be non-empty.
257+
If a chain does not agree to the proposed counterparty `UpgradedChannel`, it may abort the upgrade handshake by writing an ErrorReceipt into the `errorPath` and restoring the original channel. The ErrorReceipt must contain the current upgrade sequence on the erroring chain's channel end.
213258

214-
`errorPath(id) => error_receipt`
259+
`errorPath(portID, channelID, sequence) => ErrorReceipt(sequence, msg)`
215260

216-
A relayer may then submit a `ChanUpgradeCancelMsg` to the counterparty. Upon receiving this message a chain must verify that the counterparty wrote a non-empty error receipt into its `UpgradeError` and if successful, it will restore its original channel as well thus cancelling the upgrade.
261+
A relayer may then submit a `ChanUpgradeCancelMsg` to the counterparty. Upon receiving this message a chain must verify that the counterparty wrote an ErrorReceipt into its `UpgradeError` 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.
217262

218263
If an upgrade message arrives after the specified timeout, then the message MUST NOT execute successfully. Again a relayer may submit a proof of this in a `ChanUpgradeTimeoutMsg` so that counterparty cancels the upgrade and restores it original channel as well.
219264

@@ -253,13 +298,16 @@ function chanUpgradeInit(
253298
timeoutTimestamp: counterpartyTimeoutTimestamp,
254299
}
255300

301+
sequence := provableStore.get(upgradeSequencePath(portIdentifier, channelIdentifier))
302+
256303
// call modules onChanUpgradeInit callback
257304
module = lookupModule(portIdentifier)
258305
version, err = module.onChanUpgradeInit(
259306
proposedUpgradeChannel.ordering,
260307
proposedUpgradeChannel.connectionHops,
261308
portIdentifier,
262309
channelIdentifer,
310+
sequence,
263311
proposedUpgradeChannel.counterpartyPortIdentifer,
264312
proposedUpgradeChannel.counterpartyChannelIdentifier,
265313
proposedUpgradeChannel.version
@@ -285,11 +333,13 @@ function chanUpgradeTry(
285333
portIdentifier: Identifier,
286334
channelIdentifier: Identifier,
287335
counterpartyChannel: ChannelEnd,
336+
counterpartySequence: uint64,
288337
proposedUpgradeChannel: ChannelEnd,
289338
timeoutHeight: Height,
290339
timeoutTimestamp: uint64,
291340
proofChannel: CommitmentProof,
292341
proofUpgradeTimeout: CommitmentProof,
342+
proofUpgradeSequence: CommitmentProof,
293343
proofHeight: Height
294344
) {
295345
// current channel must be OPEN or UPGRADE_INIT (crossing hellos)
@@ -324,6 +374,28 @@ function chanUpgradeTry(
324374
// verify proofs of counterparty state
325375
abortTransactionUnless(verifyChannelState(connection, proofHeight, proofChannel, currentChannel.counterpartyPortIdentifier, currentChannel.counterpartyChannelIdentifier, counterpartyChannel))
326376
abortTransactionUnless(verifyChannelUpgradeTimeout(connection, proofHeight, proofUpgradeTimeout, currentChannel.counterpartyPortIdentifier, currentChannel.counterpartyChannelIdentifier, upgradeTimeout))
377+
abortTransactionUnless(verifyUpgradeSequence(connection, proofHeight, proofUpgradeSequence, currentChannel.counterpartyPortIdentifier,
378+
currentChannel.counterpartyChannelIdentifier, counterpartySequence))
379+
380+
// get current sequence on this channel
381+
// if the counterparty sequence is greater than the current sequence, we fast forward to the counterparty sequence
382+
// so that both channel ends are using the same sequence for the current upgrade
383+
// if the counterparty sequence is less than the current sequence, then either the counterparty chain is out-of-sync or
384+
// the message is out-of-sync and we write an error receipt with our own sequence so that the counterparty can update
385+
// their sequence as well. We must then increment our sequence so both sides start the next upgrade with a fresh sequence.
386+
currentSequence = provableStore.get(upgradeSequencePath(portIdentifier, channelIdentifier))
387+
if counterpartySequence >= currentSequence {
388+
provableStore.set(upgradeSequencePath(portIdentifier, channelIdentifier), counterpartySequence)
389+
} else {
390+
// error on the higher sequence so that both chains move to a fresh sequence
391+
errorReceipt = ErrorReceipt{
392+
Sequence: currentSequence,
393+
ErrorMsg: ""
394+
}
395+
provableStore.set(errorPath(portIdentifier, channelIdentifier), errorReceipt)
396+
provableStore.set(upgradeSequencePath(portIdentifier, channelIdentifier), currentSequence+1)
397+
return
398+
}
327399

328400
if currentChannel.state == UPGRADE_INIT {
329401
// if there is a crossing hello, ie an UpgradeInit has been called on both channelEnds,
@@ -376,6 +448,7 @@ function chanUpgradeTry(
376448
proposedUpgradeChannel.connectionHops,
377449
portIdentifier,
378450
channelIdentifer,
451+
currentSequence,
379452
proposedUpgradeChannel.counterpartyPortIdentifer,
380453
proposedUpgradeChannel.counterpartyChannelIdentifier,
381454
proposedUpgradeChannel.version
@@ -403,6 +476,7 @@ function chanUpgradeAck(
403476
channelIdentifier: Identifier,
404477
counterpartyChannel: ChannelEnd,
405478
proofChannel: CommitmentProof,
479+
proofUpgradeSequence: CommitmentProof,
406480
proofHeight: Height
407481
) {
408482
// current channel is in UPGRADE_INIT or UPGRADE_TRY (crossing hellos)
@@ -415,6 +489,14 @@ function chanUpgradeAck(
415489
// verify proofs of counterparty state
416490
abortTransactionUnless(verifyChannelState(connection, proofHeight, proofChannel, currentChannel.counterpartyPortIdentifier, currentChannel.counterpartyChannelIdentifier, counterpartyChannel))
417491

492+
// verify that the counterparty sequence is the same as the current sequence to ensure that the proofs were
493+
// retrieved from the current upgrade attempt
494+
// since all proofs are retrieved from same proof height, and there can not be multiple upgrade states in the store for a given
495+
// channel at the same time
496+
sequence = provableStore.get(upgradeSequencePath(portIdentifier, channelIdentifier))
497+
abortTransactionUnless(verifyUpgradeSequence(connection, proofHeight, proofUpgradeSequence, currentChannel.counterpartyPortIdentifier,
498+
currentChannel.counterpartyChannelIdentifier, sequence))
499+
418500
// counterparty must be in TRY state
419501
if counterpartyChannel.State != UPGRADE_TRY {
420502
restoreChannel()
@@ -451,6 +533,9 @@ function chanUpgradeAck(
451533
provableStore.set(channelPath(portIdentifier, channelIdentifier), currentChannel)
452534
provableStore.delete(timeoutPath(portIdentifier, channelIdentifier))
453535
privateStore.delete(restorePath(portIdentifier, channelIdentifier))
536+
537+
// increment sequence in preparation for the next upgrade
538+
provableStore.set(upgradeSequencePath(portIdentifier, channelIdentifier), sequence+1)
454539
}
455540
```
456541

@@ -461,6 +546,7 @@ function chanUpgradeConfirm(
461546
counterpartyChannel: ChannelEnd,
462547
proofChannel: CommitmentProof,
463548
proofUpgradeError: CommitmentProof,
549+
proofUpgradeSequence: CommitmentProof,
464550
proofHeight: Height,
465551
) {
466552
// current channel is in UPGRADE_TRY
@@ -470,15 +556,25 @@ function chanUpgradeConfirm(
470556
// counterparty must be in OPEN state
471557
abortTransactionUnless(counterpartyChannel.State == OPEN)
472558

559+
// get current sequence
560+
sequence = provableStore.get(upgradeSequencePath(portIdentifier, channelIdentifier))
561+
473562
// get underlying connection for proof verification
474563
connection = getConnection(currentChannel.connectionIdentifier)
475564

476565
// verify proofs of counterparty state
477566
abortTransactionUnless(verifyChannelState(connection, proofHeight, proofChannel, currentChannel.counterpartyPortIdentifier, currentChannel.counterpartyChannelIdentifier, counterpartyChannel))
478567
// verify counterparty did not abort upgrade handshake by writing upgrade error
479-
// must have absent value at upgradeError path
568+
// must have absent value at upgradeError path at the current sequence
480569
abortTransactionUnless(verifyUpgradeChannelErrorAbsence(connection, proofHeight, proofUpgradeError, currentChannel.counterpartyPortIdentifier, currentChannel.counterpartyChannelIdentifier))
481570

571+
// verify that the counterparty sequence is the same as the current sequence to ensure that the proofs were
572+
// retrieved from the current upgrade attempt
573+
// since all proofs are retrieved from same proof height, and there can not be multiple upgrade states in the store for a given
574+
// channel at the same time
575+
abortTransactionUnless(verifyUpgradeSequence(connection, proofHeight, proofUpgradeSequence, currentChannel.counterpartyPortIdentifier,
576+
currentChannel.counterpartyChannelIdentifier, sequence))
577+
482578
// call modules onChanUpgradeConfirm callback
483579
module = lookupModule(portIdentifier)
484580
// confirm callback must not return error since counterparty successfully upgraded
@@ -493,6 +589,9 @@ function chanUpgradeConfirm(
493589
provableStore.set(channelPath(portIdentifier, channelIdentifier), currentChannel)
494590
provableStore.delete(timeoutPath(portIdentifier, channelIdentifier))
495591
privateStore.delete(restorePath(portIdentifier, channelIdentifier))
592+
593+
// increment sequence in preparation for the next upgrade
594+
provableStore.set(upgradeSequencePath(portIdentifier, channelIdentifier), sequence+1)
496595
}
497596
```
498597

@@ -505,7 +604,7 @@ During the upgrade handshake a chain may cancel the upgrade by writing an error
505604
function cancelChannelUpgrade(
506605
portIdentifier: Identifier,
507606
channelIdentifier: Identifier,
508-
errorReceipt: []byte,
607+
errorReceipt: ErrorReceipt,
509608
proofUpgradeError: CommitmentProof,
510609
proofHeight: Height,
511610
) {
@@ -515,9 +614,16 @@ function cancelChannelUpgrade(
515614

516615
abortTransactionUnless(!isEmpty(errorReceipt))
517616

617+
// get current sequence
618+
// If counterparty sequence is less than the current sequence, abort transaction since this error receipt is from a previous upgrade
619+
// Otherwise, set the sequence to counterparty's error sequence+1 so that both sides start with a fresh sequence
620+
currentSequence = provableStore.get(currentSequencePath(portIdentifier, channelIdentifier))
621+
abortTransactionUnless(errorReceipt.Sequence >= currentSequence)
622+
provableStore.set(upgradeSequencePath(portIdentifier, channelIdentifier), errorReceipt.Sequence+1)
623+
518624
// get underlying connection for proof verification
519625
connection = getConnection(currentChannel.connectionIdentifier)
520-
// verify that a non-empty error receipt is written to the upgradeError path
626+
// verify that the provided error receipt is written to the upgradeError path with the counterparty sequence
521627
abortTransactionUnless(verifyChannelUpgradeError(connection, proofHeight, proofUpgradeError, currentChannel.counterpartyPortIdentifier, currentChannel.counterpartyChannelIdentifier, errorReceipt))
522628

523629
// cancel upgrade
@@ -553,7 +659,9 @@ function timeoutChannelUpgrade(
553659
portIdentifier: Identifier,
554660
channelIdentifier: Identifier,
555661
counterpartyChannel: ChannelEnd,
662+
prevErrorReceipt: ErrorReceipt, // optional
556663
proofChannel: CommitmentProof,
664+
proofErrorReceipt: CommitmentProof,
557665
proofHeight: Height,
558666
) {
559667
// current channel must be in UPGRADE_INIT
@@ -577,12 +685,15 @@ function timeoutChannelUpgrade(
577685
abortTransactionUnless(counterpartyChannel.State === OPEN || counterpartyChannel.State == UPGRADE_INIT)
578686
abortTransactionUnless(verifyChannelState(connection, proofHeight, proofChannel, currentChannel.counterpartyPortIdentifier, currentChannel.counterpartyChannelIdentifier, counterpartyChannel))
579687

580-
if counterpartyChannel.State == UPGRADE_INIT {
581-
// if the counterparty is in UPGRADE_INIT and we have timed out then we should write and error receipt
582-
// to ensure that counterparty aborts the handshake as well and returns to the original state
583-
// write an error receipt into the error path
584-
errorReceipt = []byte{1}
585-
provableStore.set(errorPath(portIdentifier, channelIdentifier), errorReceipt)
688+
// Error receipt passed in is either nil or it is a stale error receipt from a previous upgrade
689+
if prevErrorReceipt == nil {
690+
abortTransactionUnless(verifyErrorReceiptAbsence(connection, proofHeight, proofErrorReceipt, currentChannel.counterpartyPortIdentifier, currentChannel.counterpartyChannelIdentifier))
691+
} else {
692+
// timeout for this sequence can only succeed if the error receipt written into the error path on the counterparty
693+
// was for a previous sequence by the timeout deadline.
694+
sequence = provableStore.get(upgradeSequencePath(portIdentifier, channelIdentifier))
695+
abortTransactionUnless(sequence > prevErrorReceipt.sequence)
696+
abortTransactionUnless(verifyErrorReceipt(connection, proofHeight, proofErrorReceipt, currentChannel.counterpartyPortIdentifier, currentChannel.counterpartyChannelIdentifier, prevErrorReceipt))
586697
}
587698

588699
// we must restore the channel since the timeout verification has passed
@@ -605,6 +716,8 @@ function timeoutChannelUpgrade(
605716

606717
Note that the timeout logic only applies to the INIT step. This is to protect an upgrading chain from being stuck in a non-OPEN state if the counterparty cannot execute the TRY successfully. Once the TRY step succeeds, then both sides are guaranteed to have the upgrade feature enabled. Liveness is no longer an issue, because we can wait until liveness is restored to execute the ACK step which will move the channel definitely into an OPEN state (either a successful upgrade or a rollback).
607718

719+
The error receipt on the counterparty may be empty (either because an upgrade error did not occur in the past, or a previous attempt was pruned), or it may have an outdated sequence (in this case the counterparty errored, our side executed a `CancelUpgrade`, and then subsequently executed `INIT`). In the case where the error receipt is empty, the relayer is expected to submit an absence proof in the timeout message. In the case where the error receipt is for an outdated sequence, the relayer is expected to submit an existence proof in the timeout message. In this case, the handler will assert that the counterparty sequence is outdated **and** the upgrade timeout has passed on the counterparty by the proof height; thus proving that the counterparty did not receive a timeout message within the valid window.
720+
608721
The TRY chain will receive the timeout parameters chosen by the counterparty on INIT, so that it can reject any TRY message that is received after the specified timeout. This prevents the handshake from entering into an invalid state, in which the INIT chain processes a timeout successfully and restores its channel to `OPEN` while the TRY chain at a later point successfully writes a `TRY` state.
609722

610723
### Migrations

0 commit comments

Comments
 (0)