Skip to content

Commit d64734b

Browse files
committed
Add channel_reestablish TLVs for retransmission
With splicing, we introduce new TLVs to `channel_reestablish` to let our peer know: - the latest `splice_locked` (or `channel_ready`) we're ready to send or have sent before disconnecting - whether we need a retransmission of `commit_sig` for the next funding - whether we need a retransmission of `announcement_signatures` for the next funding (always false for mobile wallets) This lets us clean-up retransmission of those messages and follow the official splicing spec.
1 parent ab703ae commit d64734b

14 files changed

Lines changed: 232 additions & 161 deletions

File tree

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,8 @@ data class Commitments(
707707
// We always use the last commitment that was created, to make sure we never go back in time.
708708
val latest = FullCommitment(channelParams, changes, active.first())
709709

710+
fun lastLocalLocked(zeroConf: Boolean): Commitment? = active.find { zeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx }
711+
710712
val all = buildList {
711713
addAll(active)
712714
addAll(inactive)
@@ -1084,7 +1086,7 @@ data class Commitments(
10841086
// This ensures that we only have to send splice_locked for the latest commitment instead of sending it for every commitment.
10851087
// A side-effect is that previous commitments that are implicitly locked don't necessarily have their status correctly set.
10861088
// That's why we look at locked commitments separately and then select the one with the oldest fundingTxIndex.
1087-
val lastLocalLocked = active.find { staticParams.useZeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx }
1089+
val lastLocal = lastLocalLocked(staticParams.useZeroConf)
10881090
val lastRemoteLocked = active.find { it.remoteFundingStatus == RemoteFundingStatus.Locked }
10891091
return when {
10901092
// We select the locked commitment with the smaller value for fundingTxIndex, but both have to be defined.
@@ -1093,9 +1095,9 @@ data class Commitments(
10931095
// - transactions with the same fundingTxIndex double-spend each other, so only one of them can confirm
10941096
// - we don't allow creating a splice on top of an unconfirmed transaction that has RBF attempts (because it
10951097
// would become invalid if another of the RBF attempts end up being confirmed)
1096-
lastLocalLocked != null && lastRemoteLocked != null -> listOf(lastLocalLocked, lastRemoteLocked).minByOrNull { it.fundingTxIndex }
1098+
lastLocal != null && lastRemoteLocked != null -> listOf(lastLocal, lastRemoteLocked).minByOrNull { it.fundingTxIndex }
10971099
// Special case for the initial funding tx, we only require a local lock because channel_ready doesn't explicitly reference a funding tx.
1098-
lastLocalLocked != null && lastLocalLocked.fundingTxIndex == 0L -> lastLocalLocked
1100+
lastLocal != null && lastLocal.fundingTxIndex == 0L -> lastLocal
10991101
else -> null
11001102
}
11011103
}

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,11 +1106,8 @@ data class InteractiveTxSigningSession(
11061106
// +-------+ +-------+
11071107
val fundingTxId: TxId = fundingTx.txId
11081108
val localCommitIndex = localCommit.fold({ it.index }, { it.index })
1109-
// This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not.
1110-
val nextLocalCommitmentNumber = when (localCommit) {
1111-
is Either.Left -> localCommit.value.index
1112-
is Either.Right -> localCommit.value.index + 1
1113-
}
1109+
// If we haven't received the remote commit_sig, we will request a retransmission on reconnection.
1110+
val retransmitRemoteCommitSig: Boolean = localCommit.isLeft
11141111

11151112
fun localFundingKey(channelKeys: ChannelKeys): PrivateKey = fundingParams.fundingKey(channelKeys)
11161113

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -317,37 +317,27 @@ sealed class PersistedChannelState : ChannelState() {
317317
}
318318
ChannelReestablish(
319319
channelId = channelId,
320-
nextLocalCommitmentNumber = state.signingSession.nextLocalCommitmentNumber,
320+
nextLocalCommitmentNumber = 1,
321321
nextRemoteRevocationNumber = 0,
322322
yourLastCommitmentSecret = PrivateKey(ByteVector32.Zeroes),
323323
myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint,
324324
nextCommitNonces = nextCommitNonce?.let { listOf(nextFundingTxId to it) } ?: listOf(),
325325
nextFundingTxId = nextFundingTxId,
326+
retransmitCommitSig = state.signingSession.retransmitRemoteCommitSig,
326327
currentCommitNonce = currentCommitNonce
327328
)
328329
}
329330
is ChannelStateWithCommitments -> {
330331
val channelKeys = channelKeys()
331332
val yourLastPerCommitmentSecret = state.commitments.remotePerCommitmentSecrets.lastIndex?.let { state.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes
332333
val myCurrentPerCommitmentPoint = channelKeys.commitmentPoint(state.commitments.localCommitIndex)
333-
// If we disconnected while signing a funding transaction, we may need our peer to retransmit their commit_sig.
334-
val nextLocalCommitmentNumber = when (state) {
335-
is WaitForFundingConfirmed -> when (state.rbfStatus) {
336-
is RbfStatus.WaitingForSigs -> state.rbfStatus.session.nextLocalCommitmentNumber
337-
else -> state.commitments.localCommitIndex + 1
338-
}
339-
is Normal -> when (state.spliceStatus) {
340-
is SpliceStatus.WaitingForSigs -> state.spliceStatus.session.nextLocalCommitmentNumber
341-
else -> state.commitments.localCommitIndex + 1
342-
}
343-
else -> state.commitments.localCommitIndex + 1
344-
}
345-
// If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures.
346-
val unsignedFundingTxId = when (state) {
334+
// If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures and commit_sig.
335+
val (unsignedFundingTxId, retransmitCommitSig) = when (state) {
347336
is WaitForFundingConfirmed -> state.getUnsignedFundingTxId()
348337
is Normal -> state.getUnsignedFundingTxId()
349-
else -> null
338+
else -> Pair(null, false)
350339
}
340+
val lastFundingLocked = state.commitments.lastLocalLocked(staticParams.useZeroConf)
351341
// We send our verification nonces for all active commitments.
352342
val nextCommitNonces = state.commitments.active.mapNotNull { c ->
353343
when (c.commitmentFormat) {
@@ -378,12 +368,14 @@ sealed class PersistedChannelState : ChannelState() {
378368
}
379369
ChannelReestablish(
380370
channelId = channelId,
381-
nextLocalCommitmentNumber = nextLocalCommitmentNumber,
371+
nextLocalCommitmentNumber = state.commitments.localCommitIndex + 1,
382372
nextRemoteRevocationNumber = state.commitments.remoteCommitIndex,
383373
yourLastCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret),
384374
myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint,
385375
nextCommitNonces = nextCommitNonces + listOfNotNull(interactiveTxNextCommitNonce),
386376
nextFundingTxId = unsignedFundingTxId,
377+
retransmitCommitSig = retransmitCommitSig,
378+
currentFundingLocked = lastFundingLocked?.fundingTxId,
387379
currentCommitNonce = interactiveTxCurrentCommitNonce,
388380
)
389381
}

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -935,11 +935,14 @@ data class Normal(
935935
return Pair(nextState, actions)
936936
}
937937

938-
/** If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. */
939-
fun getUnsignedFundingTxId(): TxId? = when {
940-
spliceStatus is SpliceStatus.WaitingForSigs -> spliceStatus.session.fundingTx.txId
941-
commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx && commitments.latest.localFundingStatus.sharedTx is PartiallySignedSharedTransaction -> commitments.latest.localFundingStatus.txId
942-
else -> null
938+
/**
939+
* If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction.
940+
* The second parameter should be set to true when commit_sig also needs to be retransmitted.
941+
*/
942+
fun getUnsignedFundingTxId(): Pair<TxId?, Boolean> = when {
943+
spliceStatus is SpliceStatus.WaitingForSigs -> Pair(spliceStatus.session.fundingTx.txId, spliceStatus.session.retransmitRemoteCommitSig)
944+
commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx && commitments.latest.localFundingStatus.sharedTx is PartiallySignedSharedTransaction -> Pair(commitments.latest.localFundingStatus.txId, false)
945+
else -> Pair(null, false)
943946
}
944947

945948
/** Returns true if the [shortChannelId] matches one of our commitments or our alias. */

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent:
2525
val (nextState, actions) = when (state) {
2626
is WaitForFundingSigned -> {
2727
val actions = buildList {
28-
if (cmd.message.nextFundingTxId == state.signingSession.fundingTx.txId && cmd.message.nextLocalCommitmentNumber == 0L) {
28+
if (cmd.message.nextFundingTxId == state.signingSession.fundingTx.txId && cmd.message.retransmitInteractiveTxCommitSig) {
2929
// They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received
3030
// their commit_sig or their tx_signatures (depending on who must send tx_signatures first).
3131
logger.info { "re-sending commit_sig for channel creation with fundingTxId=${state.signingSession.fundingTx.txId}" }
@@ -44,7 +44,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent:
4444
else -> {
4545
if (state.rbfStatus is RbfStatus.WaitingForSigs && state.rbfStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) {
4646
val actions = buildList {
47-
if (cmd.message.nextLocalCommitmentNumber == 0L) {
47+
if (cmd.message.retransmitInteractiveTxCommitSig) {
4848
// They haven't received our commit_sig: we retransmit it.
4949
// We're waiting for signatures from them, and will send our tx_signatures once we receive them.
5050
logger.info { "re-sending commit_sig for rbf attempt with fundingTxId=${cmd.message.nextFundingTxId}" }
@@ -59,7 +59,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent:
5959
// We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
6060
// and our commit_sig if they haven't received it already.
6161
val actions = buildList {
62-
if (cmd.message.nextLocalCommitmentNumber == 0L) {
62+
if (cmd.message.retransmitInteractiveTxCommitSig) {
6363
logger.info { "re-sending commit_sig for fundingTxId=${cmd.message.nextFundingTxId}" }
6464
when (val commitSig = state.commitments.latest.remoteCommit.sign(
6565
state.commitments.channelParams,
@@ -91,11 +91,10 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent:
9191
}
9292
is WaitForChannelReady -> {
9393
val actions = ArrayList<ChannelAction>()
94-
// We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
95-
// and our commit_sig if they haven't received it already.
94+
// If they haven't received our signatures for the channel funding transaction, we retransmit them.
9695
if (state.commitments.latest.fundingTxId == cmd.message.nextFundingTxId) {
9796
if (state.commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx) {
98-
if (cmd.message.nextLocalCommitmentNumber == 0L) {
97+
if (cmd.message.retransmitInteractiveTxCommitSig) {
9998
logger.info { "re-sending commit_sig for fundingTxId=${state.commitments.latest.fundingTxId}" }
10099
when (val commitSig = state.commitments.latest.remoteCommit.sign(
101100
state.commitments.channelParams,
@@ -143,7 +142,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent:
143142

144143
// resume splice signing session if any
145144
val spliceStatus1 = if (state.spliceStatus is SpliceStatus.WaitingForSigs && state.spliceStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) {
146-
if (cmd.message.nextLocalCommitmentNumber == state.commitments.remoteCommitIndex) {
145+
if (cmd.message.retransmitInteractiveTxCommitSig) {
147146
// They haven't received our commit_sig: we retransmit it.
148147
// We're waiting for signatures from them, and will send our tx_signatures once we receive them.
149148
logger.info { "re-sending commit_sig for splice attempt with fundingTxIndex=${state.spliceStatus.session.fundingParams.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" }
@@ -158,7 +157,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent:
158157
is LocalFundingStatus.UnconfirmedFundingTx -> {
159158
// We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
160159
// and our commit_sig if they haven't received it already.
161-
if (cmd.message.nextLocalCommitmentNumber == state.commitments.remoteCommitIndex) {
160+
if (cmd.message.retransmitInteractiveTxCommitSig) {
162161
logger.info { "re-sending commit_sig for fundingTxIndex=${state.commitments.latest.fundingTxIndex} fundingTxId=${state.commitments.latest.fundingTxId}" }
163162
when (val commitSig = state.commitments.latest.remoteCommit.sign(
164163
state.commitments.channelParams,
@@ -177,6 +176,8 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent:
177176
}
178177
logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" }
179178
actions.add(ChannelAction.Message.Send(localFundingStatus.sharedTx.localSigs))
179+
// If we're using 0-conf, we also retransmit our splice_locked.
180+
if (staticParams.useZeroConf) actions.add(ChannelAction.Message.Send(SpliceLocked(channelId, cmd.message.nextFundingTxId)))
180181
}
181182
is LocalFundingStatus.ConfirmedFundingTx -> {
182183
// The funding tx is confirmed, and they have not received our tx_signatures, but they must have received our commit_sig, otherwise they
@@ -195,26 +196,27 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent:
195196
state.spliceStatus
196197
}
197198

198-
// Re-send splice_locked (must come *after* potentially retransmitting tx_signatures).
199-
// NB: there is a key difference between channel_ready and splice_locked:
200-
// - channel_ready: a non-zero commitment index implies that both sides have seen the channel_ready
201-
// - splice_locked: the commitment index can be updated as long as it is compatible with all splices, so
202-
// we must keep sending our most recent splice_locked at each reconnection
203-
state.commitments.active
204-
.filter { it.fundingTxIndex > 0L } // only consider splice txs
205-
.firstOrNull { staticParams.useZeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx }
206-
?.let {
207-
logger.debug { "re-sending splice_locked for fundingTxId=${it.fundingTxId}" }
208-
val spliceLocked = SpliceLocked(channelId, it.fundingTxId)
209-
actions.add(ChannelAction.Message.Send(spliceLocked))
199+
// Prune previous funding transactions and RBF attempts if we already sent splice_locked for the last funding
200+
// transaction that is also locked by our counterparty; we either missed their splice_locked or it confirmed
201+
// while disconnected.
202+
val commitments1 = run {
203+
val withRemoteLocked = when (val remoteFundingTxLocked = cmd.message.myCurrentFundingLocked) {
204+
null -> state.commitments
205+
else -> when (val commitments1 = state.commitments.run { updateRemoteFundingStatus(remoteFundingTxLocked) }) {
206+
is Either.Left -> state.commitments
207+
is Either.Right -> {
208+
state.run { newlyLocked(state.commitments, commitments1.value.first) }.forEach { actions.add(ChannelAction.Storage.SetLocked(it.fundingTxId)) }
209+
commitments1.value.first
210+
}
211+
}
210212
}
213+
// Then we clean up unsigned updates.
214+
discardUnsignedUpdates(withRemoteLocked)
215+
}
211216

212217
// we may need to retransmit updates and/or commit_sig and/or revocation
213218
actions.addAll(syncResult.retransmit.map { ChannelAction.Message.Send(it) })
214219

215-
// then we clean up unsigned updates
216-
val commitments1 = discardUnsignedUpdates(state.commitments)
217-
218220
if (commitments1.changes.localHasChanges()) {
219221
actions.add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign))
220222
}

0 commit comments

Comments
 (0)