@@ -15,8 +15,10 @@ import fr.acinq.lightning.channel.Helpers.Closing.claimRevokedRemoteCommitTxHtlc
1515import fr.acinq.lightning.channel.Helpers.Closing.extractPreimages
1616import fr.acinq.lightning.channel.Helpers.Closing.onChainOutgoingHtlcs
1717import fr.acinq.lightning.channel.Helpers.Closing.overriddenOutgoingHtlcs
18- import fr.acinq.lightning.channel.Helpers.Closing.timedOutHtlcs
18+ import fr.acinq.lightning.channel.Helpers.Closing.trimmedOrTimedOutHtlcs
1919import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx
20+ import fr.acinq.lightning.transactions.incomings
21+ import fr.acinq.lightning.transactions.outgoings
2022import fr.acinq.lightning.utils.getValue
2123import fr.acinq.lightning.wire.ChannelReestablish
2224import fr.acinq.lightning.wire.Error
@@ -106,20 +108,38 @@ data class Closing(
106108 // This commitment may be revoked: we need to verify that its index matches our latest known index before overwriting our previous commitments.
107109 when {
108110 watch.tx.txid == commitments1.latest.localCommit.publishableTxs.commitTx.tx.txid -> {
109- // our local commit has been published from the outside, it's unexpected but let's deal with it anyway
111+ // Our local commit has been published from the outside, it's unexpected but let's deal with it anyway.
110112 newState.run { spendLocalCurrent() }
111113 }
112114 watch.tx.txid == commitments1.latest.remoteCommit.txid && commitments1.remoteCommitIndex == commitments.remoteCommitIndex -> {
113- // counterparty may attempt to spend its last commit tx at any time
115+ // Our counterparty may attempt to spend its last commit tx at any time.
114116 newState.run { handleRemoteSpentCurrent(watch.tx, commitments1.latest) }
115117 }
116118 watch.tx.txid == commitments1.latest.nextRemoteCommit?.commit?.txid && commitments1.remoteCommitIndex == commitments.remoteCommitIndex && commitments.remoteNextCommitInfo.isLeft -> {
117- // counterparty may attempt to spend its next commit tx at any time
119+ // Our counterparty may attempt to spend its next commit tx at any time.
118120 newState.run { handleRemoteSpentNext(watch.tx, commitments1.latest) }
119121 }
120122 else -> {
121- // counterparty may attempt to spend a revoked commit tx at any time
122- newState.run { handleRemoteSpentOther(watch.tx) }
123+ // Our counterparty is trying to broadcast a revoked commit tx (cheating attempt).
124+ // We need to fail pending outgoing HTLCs, otherwise we will never properly settle them.
125+ // We must do it here because since we're overwriting the commitments data, we will lose all information
126+ // about HTLCs that are in the current commitments but were not in the revoked one.
127+ // We fail *all* outgoing HTLCs:
128+ // - those that are not in the revoked commitment will never settle on-chain
129+ // - those that are in the revoked commitment will be claimed on-chain, so it's as if they were failed
130+ // Note that if we already received the preimage for some of these HTLCs, we already relayed it to the
131+ // outgoing payment handler so the fail command will be a no-op.
132+ val outgoingHtlcs = commitments.latest.localCommit.spec.htlcs.outgoings().toSet() +
133+ commitments.latest.remoteCommit.spec.htlcs.incomings().toSet() +
134+ (commitments.latest.nextRemoteCommit?.commit?.spec?.htlcs ? : setOf ()).incomings().toSet()
135+ val htlcSettledActions = outgoingHtlcs.mapNotNull { add ->
136+ commitments.payments[add.id]?.let { paymentId ->
137+ logger.info { " failing htlc #${add.id} paymentHash=${add.paymentHash} paymentId=$paymentId : overridden by revoked remote commit" }
138+ ChannelAction .ProcessCmdRes .AddSettledFail (paymentId, add, ChannelAction .HtlcResult .Fail .OnChainFail (HtlcOverriddenByLocalCommit (channelId, add)))
139+ }
140+ }
141+ val (nextState, closingActions) = newState.run { handleRemoteSpentOther(watch.tx) }
142+ Pair (nextState, closingActions + htlcSettledActions)
123143 }
124144 }
125145 }
@@ -141,15 +161,18 @@ data class Closing(
141161 // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold
142162 val htlcSettledActions = mutableListOf<ChannelAction >()
143163 val timedOutHtlcs = when (val closingType = closing1.closingTypeAlreadyKnown()) {
144- is LocalClose -> timedOutHtlcs(closingType.localCommit, closingType.localCommitPublished, commitments.params.localParams.dustLimit, watch.tx)
145- is RemoteClose -> timedOutHtlcs(closingType.remoteCommit, closingType.remoteCommitPublished, commitments.params.remoteParams.dustLimit, watch.tx)
146- else -> setOf () // we lose htlc outputs in option_data_loss_protect scenarios (future remote commit)
164+ is LocalClose -> trimmedOrTimedOutHtlcs(closingType.localCommit, closingType.localCommitPublished, commitments.params.localParams.dustLimit, watch.tx)
165+ is RemoteClose -> trimmedOrTimedOutHtlcs(closingType.remoteCommit, closingType.remoteCommitPublished, commitments.params.remoteParams.dustLimit, watch.tx)
166+ is RevokedClose -> setOf () // revoked commitments are handled using [overriddenOutgoingHtlcs] below
167+ is RecoveryClose -> setOf () // we lose htlc outputs in option_data_loss_protect scenarios (future remote commit)
168+ is MutualClose -> setOf ()
169+ null -> setOf ()
147170 }
148171 timedOutHtlcs.forEach { add ->
149172 when (val paymentId = commitments.payments[add.id]) {
150173 null -> {
151174 // same as for fulfilling the htlc (no big deal)
152- logger.info { " cannot fail timedout htlc #${add.id} paymentHash=${add.paymentHash} (payment not found)" }
175+ logger.info { " cannot fail timed-out htlc #${add.id} paymentHash=${add.paymentHash} (payment not found)" }
153176 }
154177 else -> {
155178 logger.info { " failing htlc #${add.id} paymentHash=${add.paymentHash} paymentId=$paymentId : htlc timed out" }
@@ -243,7 +266,7 @@ data class Closing(
243266 // we can then use these preimages to fulfill payments
244267 logger.info { " processing spent closing output with txid=${watch.spendingTx.txid} tx=${watch.spendingTx} " }
245268 val htlcSettledActions = mutableListOf<ChannelAction >()
246- extractPreimages(commitments.latest.localCommit , watch.spendingTx).forEach { (htlc, preimage) ->
269+ extractPreimages(commitments.latest, watch.spendingTx).forEach { (htlc, preimage) ->
247270 when (val paymentId = commitments.payments[htlc.id]) {
248271 null -> {
249272 // if we don't have a reference to the payment, it means that we already have forwarded the fulfill so that's not a big deal.
0 commit comments