@@ -222,6 +222,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
222222 var announcementSigsStash = Map .empty[RealShortChannelId , AnnouncementSignatures ]
223223 // we record the announcement_signatures messages we already sent to avoid unnecessary retransmission
224224 var announcementSigsSent = Set .empty[RealShortChannelId ]
225+ // we keep track of the splice_locked we sent after channel_reestablish and it's funding tx index to avoid sending it again
226+ private var spliceLockedSent = Map .empty[TxId , Long ]
225227
226228 private def trimAnnouncementSigsStashIfNeeded (): Unit = {
227229 if (announcementSigsStash.size >= 10 ) {
@@ -233,6 +235,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
233235 }
234236 }
235237
238+ private def trimSpliceLockedSentIfNeeded (): Unit = {
239+ if (spliceLockedSent.size >= 10 ) {
240+ // We shouldn't store an unbounded number of splice_locked: on long-lived connections where we do a lot of splice
241+ // transactions, we only need to keep track of the most recent ones.
242+ val oldestFundingTxId = spliceLockedSent.toSeq
243+ .sortBy { case (_, fundingTxIndex) => fundingTxIndex }
244+ .map { case (fundingTxId, _) => fundingTxId }.head
245+ spliceLockedSent -= oldestFundingTxId
246+ }
247+ }
248+
236249 val txPublisher = txPublisherFactory.spawnTxPublisher(context, remoteNodeId)
237250
238251 // this will be used to detect htlc timeouts
@@ -775,10 +788,15 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
775788
776789 case Event (c : CurrentFeerates .BitcoinCore , d : DATA_NORMAL ) => handleCurrentFeerate(c, d)
777790
778- case Event (_ : ChannelReady , _ : DATA_NORMAL ) =>
779- // This happens on reconnection, because channel_ready is sent again if the channel hasn't been used yet,
780- // otherwise we cannot be sure that it was correctly received before disconnecting.
781- stay()
791+ case Event (_ : ChannelReady , d : DATA_NORMAL ) =>
792+ // After a reconnection, if the channel hasn't been used yet, our peer cannot be sure we received their channel_ready
793+ // so they will resend it. Their remote funding status must also be set to Locked if it wasn't already.
794+ // NB: Their remote funding status will be stored when the commitment is next updated, or channel_ready will
795+ // be sent again if a reconnection occurs first.
796+ stay() using d.copy(commitments = d.commitments.copy(active = d.commitments.active.map {
797+ case c if c.fundingTxIndex == 0 => c.copy(remoteFundingStatus = RemoteFundingStatus .Locked )
798+ case c => c
799+ }))
782800
783801 // Channels are publicly announced if both parties want it: we ignore this message if we don't want to announce the channel.
784802 case Event (remoteAnnSigs : AnnouncementSignatures , d : DATA_NORMAL ) if d.commitments.announceChannel =>
@@ -1341,11 +1359,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
13411359 case Event (w : WatchPublishedTriggered , d : DATA_NORMAL ) =>
13421360 val fundingStatus = LocalFundingStatus .ZeroconfPublishedFundingTx (w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid))
13431361 d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus, d.lastAnnouncedFundingTxId_opt) match {
1344- case Right ((commitments1, _ )) =>
1362+ case Right ((commitments1, commitment )) =>
13451363 // This is a zero-conf channel, the min-depth isn't critical: we use the default.
13461364 watchFundingConfirmed(w.tx.txid, Some (nodeParams.channelConf.minDepth), delay_opt = None )
13471365 maybeEmitEventsPostSplice(d.aliases, d.commitments, commitments1, d.lastAnnouncement_opt)
13481366 maybeUpdateMaxHtlcAmount(d.channelUpdate.htlcMaximumMsat, commitments1)
1367+ spliceLockedSent += (commitment.fundingTxId -> commitment.fundingTxIndex)
1368+ trimSpliceLockedSentIfNeeded()
13491369 stay() using d.copy(commitments = commitments1) storing() sending SpliceLocked (d.channelId, w.tx.txid)
13501370 case Left (_) => stay()
13511371 }
@@ -1356,7 +1376,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
13561376 // We check if this commitment was already locked before receiving the event (which happens when using 0-conf
13571377 // or for the initial funding transaction). If it was previously not locked, we must send splice_locked now.
13581378 val previouslyNotLocked = d.commitments.all.exists(c => c.fundingTxId == commitment.fundingTxId && c.localFundingStatus.isInstanceOf [LocalFundingStatus .NotLocked ])
1359- val spliceLocked_opt = if (previouslyNotLocked) Some (SpliceLocked (d.channelId, w.tx.txid)) else None
1379+ val spliceLocked_opt = if (previouslyNotLocked) {
1380+ spliceLockedSent += (commitment.fundingTxId -> commitment.fundingTxIndex)
1381+ trimSpliceLockedSentIfNeeded()
1382+ Some (SpliceLocked (d.channelId, w.tx.txid))
1383+ } else None
13601384 // If the channel is public and we've received the remote splice_locked, we send our announcement_signatures
13611385 // in order to generate the channel_announcement.
13621386 val remoteLocked = commitment.fundingTxIndex == 0 || d.commitments.all.exists(c => c.fundingTxId == commitment.fundingTxId && c.remoteFundingStatus == RemoteFundingStatus .Locked )
@@ -1379,19 +1403,34 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
13791403 case Event (msg : SpliceLocked , d : DATA_NORMAL ) =>
13801404 d.commitments.updateRemoteFundingStatus(msg.fundingTxId, d.lastAnnouncedFundingTxId_opt) match {
13811405 case Right ((commitments1, commitment)) =>
1406+ // If we have both already sent splice_locked for this commitment, then we are receiving splice_locked
1407+ // again after a reconnection and must retransmit our splice_locked and new announcement_signatures. Nodes
1408+ // retransmit splice_locked after a reconnection when they have received splice_locked but NOT matching signatures
1409+ // before the last disconnect. If a matching splice_locked has already been sent since reconnecting, then do not
1410+ // retransmit splice_locked to avoid a loop.
1411+ // NB: It is important both nodes retransmit splice_locked after reconnecting to ensure new Taproot nonces
1412+ // are exchanged for channel announcements.
1413+ val isLatestLocked = d.commitments.lastLocalLocked_opt.exists(_.fundingTxId == msg.fundingTxId) && d.commitments.lastRemoteLocked_opt.exists(_.fundingTxId == msg.fundingTxId)
1414+ val spliceLocked_opt = if (d.commitments.announceChannel && isLatestLocked && ! spliceLockedSent.contains(commitment.fundingTxId)) {
1415+ spliceLockedSent += (commitment.fundingTxId -> commitment.fundingTxIndex)
1416+ trimSpliceLockedSentIfNeeded()
1417+ Some (SpliceLocked (d.channelId, commitment.fundingTxId))
1418+ } else {
1419+ None
1420+ }
13821421 // If the commitment is confirmed, we were waiting to receive the remote splice_locked before sending our announcement_signatures.
1383- val localAnnSigs_opt = if (d.commitments.announceChannel) commitment.signAnnouncement(nodeParams, commitments1.params) else None
1384- localAnnSigs_opt match {
1385- case Some (localAnnSigs) =>
1386- // The commitment was locked on our side and we were waiting to receive the remote splice_locked before sending our announcement_signatures.
1422+ val localAnnSigs_opt = commitment.signAnnouncement(nodeParams, commitments1.params) match {
1423+ case Some (localAnnSigs) if ! announcementSigsSent.contains(localAnnSigs.shortChannelId) =>
13871424 announcementSigsSent += localAnnSigs.shortChannelId
13881425 // If we've already received the remote announcement_signatures, we're now ready to process them.
13891426 announcementSigsStash.get(localAnnSigs.shortChannelId).foreach(self ! _)
1390- case None => // The channel is private or the commitment isn't locked on our side.
1427+ Some (localAnnSigs)
1428+ case Some (_) => None // We've already sent these announcement_signatures since the last reconnect.
1429+ case None => None // The channel is private or the commitment isn't locked on our side.
13911430 }
13921431 maybeEmitEventsPostSplice(d.aliases, d.commitments, commitments1, d.lastAnnouncement_opt)
13931432 maybeUpdateMaxHtlcAmount(d.channelUpdate.htlcMaximumMsat, commitments1)
1394- stay() using d.copy(commitments = commitments1) storing() sending localAnnSigs_opt.toSeq
1433+ stay() using d.copy(commitments = commitments1) storing() sending spliceLocked_opt.toSeq ++ localAnnSigs_opt.toSeq
13951434 case Left (_) => stay()
13961435 }
13971436
@@ -2235,13 +2274,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
22352274 }
22362275 case _ => Set .empty
22372276 }
2277+ val lastFundingLockedTlvs : Set [ChannelReestablishTlv ] =
2278+ d.commitments.lastLocalLocked_opt.map(c => ChannelReestablishTlv .MyCurrentFundingLockedTlv (c.fundingTxId)).toSet ++
2279+ d.commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv .YourLastFundingLockedTlv (c.fundingTxId)).toSet
2280+
22382281 val channelReestablish = ChannelReestablish (
22392282 channelId = d.channelId,
22402283 nextLocalCommitmentNumber = d.commitments.localCommitIndex + 1 ,
22412284 nextRemoteRevocationNumber = d.commitments.remoteCommitIndex,
22422285 yourLastPerCommitmentSecret = PrivateKey (yourLastPerCommitmentSecret),
22432286 myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint,
2244- tlvStream = TlvStream (rbfTlv)
2287+ tlvStream = TlvStream (rbfTlv ++ lastFundingLockedTlvs )
22452288 )
22462289 // we update local/remote connection-local global/local features, we don't persist it right now
22472290 val d1 = Helpers .updateFeatures(d, localInit, remoteInit)
@@ -2333,6 +2376,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
23332376 // re-send channel_ready if necessary
23342377 if (d.commitments.latest.fundingTxIndex == 0 && channelReestablish.nextLocalCommitmentNumber == 1 && d.commitments.localCommitIndex == 0 ) {
23352378 // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit channel_ready, otherwise it MUST NOT
2379+ // TODO: when the remote node enables option_splice we can use your_last_funding_locked to detect they did not receive our channel_ready.
23362380 log.debug(" re-sending channelReady" )
23372381 val channelKeyPath = keyManager.keyPath(d.commitments.params.localParams, d.commitments.params.channelConfig)
23382382 val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1 )
@@ -2379,25 +2423,39 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
23792423 case None => d.spliceStatus
23802424 }
23812425
2382- // re-send splice_locked (must come *after* potentially retransmitting tx_signatures)
2383- // NB: there is a key difference between channel_ready and splice_confirmed:
2384- // - channel_ready: a non-zero commitment index implies that both sides have seen the channel_ready
2385- // - splice_confirmed: the commitment index can be updated as long as it is compatible with all splices, so
2386- // we must keep sending our most recent splice_locked at each reconnection
2387- val spliceLocked = d.commitments.active
2388- .filter(c => c.fundingTxIndex > 0 ) // only consider splice txs
2389- .collectFirst { case c if c.localFundingStatus.isInstanceOf [LocalFundingStatus .Locked ] =>
2390- log.debug(" re-sending splice_locked for fundingTxId={}" , c.fundingTxId)
2391- SpliceLocked (d.channelId, c.fundingTxId)
2392- }
2393- sendQueue = sendQueue ++ spliceLocked
2426+ // Prune previous funding transactions and RBF attempts if we already sent splice_locked for the last funding
2427+ // transaction that is also locked by our counterparty; we either missed their splice_locked or it confirmed
2428+ // while disconnected.
2429+ val commitments1 : Commitments = channelReestablish.myCurrentFundingLocked_opt
2430+ .flatMap(remoteFundingTxLocked => d.commitments.updateRemoteFundingStatus(remoteFundingTxLocked, d.lastAnnouncedFundingTxId_opt).toOption.map(_._1))
2431+ .getOrElse(d.commitments)
2432+ // We then clean up unsigned updates that haven't been received before the disconnection.
2433+ .discardUnsignedUpdates()
2434+
2435+ val spliceLocked_opt = commitments1.lastLocalLocked_opt match {
2436+ case None => None
2437+ // We only send splice_locked for splice transactions.
2438+ case Some (c) if c.fundingTxIndex == 0 => None
2439+ case Some (c) =>
2440+ // If our peer has not received our splice_locked, we retransmit it.
2441+ val notReceivedByRemote = ! channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId)
2442+ // If this is a public channel and we haven't announced the splice, we retransmit our splice_locked and
2443+ // will exchange announcement_signatures afterwards.
2444+ val notAnnouncedYet = commitments1.announceChannel && d.lastAnnouncement_opt.forall(ann => ! c.shortChannelId_opt.contains(ann.shortChannelId))
2445+ if (notReceivedByRemote || notAnnouncedYet) {
2446+ log.debug(" re-sending splice_locked for fundingTxId={}" , c.fundingTxId)
2447+ spliceLockedSent += (c.fundingTxId -> c.fundingTxIndex)
2448+ trimSpliceLockedSentIfNeeded()
2449+ Some (SpliceLocked (d.channelId, c.fundingTxId))
2450+ } else {
2451+ None
2452+ }
2453+ }
2454+ sendQueue = sendQueue ++ spliceLocked_opt.toSeq
23942455
23952456 // we may need to retransmit updates and/or commit_sig and/or revocation
23962457 sendQueue = sendQueue ++ syncSuccess.retransmit
23972458
2398- // then we clean up unsigned updates
2399- val commitments1 = d.commitments.discardUnsignedUpdates()
2400-
24012459 commitments1.remoteNextCommitInfo match {
24022460 case Left (_) =>
24032461 // we expect them to (re-)send the revocation immediately
@@ -2877,6 +2935,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
28772935 sigStash = Nil
28782936 announcementSigsStash = Map .empty
28792937 announcementSigsSent = Set .empty
2938+ spliceLockedSent = Map .empty[TxId , Long ]
28802939 }
28812940
28822941 /*
0 commit comments