Skip to content

Commit

Permalink
Add support for using last_funding_locked tlv in channel_reestablish
Browse files Browse the repository at this point in the history
This TLV is relevant for splice funding txs only.

When the `my_current_funding_locked_txid` TLV attribute confirms the latest funding tx we prune previous funding transaction similar to receiving `splice_locked` from our peer for that txid.

When we receive `your_last_funding_locked_txid` that does not match our latest confirmed funding tx, then we know our peer did not receive our last `splice_locked` and retransmit it.

We also retransmit `splice_locked` after `channel_reestablish` if we have not received `announcement_signatures` for the latest confirmed funding tx. This is needed to prompt our peer to also retransmit their own `splice_locked` and `announcement_signatures`.

This ensures both sides will have exchanged `splice_locked` after a disconnect and will be relevant for simple taproot channels to exchange nonces.

Note: This changes previous behavior for retransmitting `splice_locked` after a disconnect. Previous behavior was susceptible to a race condition if one node sent a channel update after `channel_reestablish`, but before receiving `splice_locked` from a peer that had confirmed the latest funding tx while offline.

cf. lightning/bolts#1223
  • Loading branch information
remyers committed Feb 17, 2025
1 parent 194f673 commit 5cd1827
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,23 @@ case class Commitments(params: ChannelParams,
def resolveCommitment(shortChannelId: RealShortChannelId): Option[Commitment] = {
all.find(c => c.shortChannelId_opt.contains(shortChannelId))
}

def lastFundingLockedTlv: Set[ChannelReestablishTlv] =
if (params.remoteParams.initFeatures.hasFeature(Features.SplicePrototype) && params.localParams.initFeatures.hasFeature(Features.SplicePrototype)) {
// When no remote funding tx is locked, there is a special case for the initial funding tx which only
// requires a local lock because channel_ready doesn't explicitly reference a funding tx.
val yourLast_opt = active.filter(c => c.fundingTxIndex == 0 || c.remoteFundingStatus == RemoteFundingStatus.Locked)
.sortBy(_.fundingTxIndex)
.lastOption
.map(_.fundingTxId)
val myCurrent_opt = active.find(_.localFundingStatus.isInstanceOf[LocalFundingStatus.Locked]).map(_.fundingTxId)
(yourLast_opt, myCurrent_opt) match {
case (Some(yourLast), Some(myCurrent)) => Set(ChannelReestablishTlv.LastFundingLockedTlv(yourLast, myCurrent))
case _ => Set.empty
}
} else {
Set.empty
}
}

object Commitments {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1389,9 +1389,18 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
announcementSigsStash.get(localAnnSigs.shortChannelId).foreach(self ! _)
case None => // The channel is private or the commitment isn't locked on our side.
}
// If we already have a signed channel announcement for this commitment, then we are receiving splice_locked
// again after a reconnection and must retransmit our splice_locked and new announcement_signatures. Nodes
// retransmit splice_locked after a reconnection when they have received splice_locked but NOT matching signatures.
// NB: It is important both nodes retransmit splice_locked after reconnecting to ensure new Taproot nonces
// are exchanged for channel announcements.
val spliceLocked_opt = d.lastAnnouncement_opt.collect {
case ann if commitment.shortChannelId_opt.contains(ann.shortChannelId) =>
SpliceLocked(d.channelId, msg.fundingTxId)
}
maybeEmitEventsPostSplice(d.aliases, d.commitments, commitments1, d.lastAnnouncement_opt)
maybeUpdateMaxHtlcAmount(d.channelUpdate.htlcMaximumMsat, commitments1)
stay() using d.copy(commitments = commitments1) storing() sending localAnnSigs_opt.toSeq
stay() using d.copy(commitments = commitments1) storing() sending spliceLocked_opt.toSeq ++ localAnnSigs_opt.toSeq
case Left(_) => stay()
}

Expand Down Expand Up @@ -2227,13 +2236,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
}
case _ => Set.empty
}
val lastFundingLockedTlv = d.commitments.lastFundingLockedTlv
val channelReestablish = ChannelReestablish(
channelId = d.channelId,
nextLocalCommitmentNumber = d.commitments.localCommitIndex + 1,
nextRemoteRevocationNumber = d.commitments.remoteCommitIndex,
yourLastPerCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret),
myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint,
tlvStream = TlvStream(rbfTlv)
tlvStream = TlvStream(rbfTlv ++ lastFundingLockedTlv)
)
// we update local/remote connection-local global/local features, we don't persist it right now
val d1 = Helpers.updateFeatures(d, localInit, remoteInit)
Expand Down Expand Up @@ -2371,34 +2381,49 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
case None => d.spliceStatus
}

// Prune previous funding transactions and RBF attempts if we already sent splice_locked for the last funding
// transaction confirmed by our counterparty; we either missed their splice_locked or it confirmed while disconnected.
val commitments1: Commitments = channelReestablish.lastFundingLocked_opt.flatMap(lastFundingLocked =>
d.commitments.updateRemoteFundingStatus(lastFundingLocked.myCurrent, d.lastAnnouncedFundingTxId_opt).toOption.map(_._1))
.getOrElse(d.commitments)

// re-send splice_locked (must come *after* potentially retransmitting tx_signatures)
// NB: there is a key difference between channel_ready and splice_confirmed:
// - channel_ready: a non-zero commitment index implies that both sides have seen the channel_ready
// - splice_confirmed: the commitment index can be updated as long as it is compatible with all splices, so
// we must keep sending our most recent splice_locked at each reconnection
val spliceLocked = d.commitments.active
.filter(c => c.fundingTxIndex > 0) // only consider splice txs
.collectFirst { case c if c.localFundingStatus.isInstanceOf[LocalFundingStatus.Locked] =>
log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId)
SpliceLocked(d.channelId, c.fundingTxId)
// - splice_confirmed: the commitment index can be updated as long as it is compatible with all splices
// We must send our most recent splice_locked until our counterparty receives it and, for a public
// channel, also sends their announcement signatures.
val spliceLocked = for {
yourLast <- channelReestablish.lastFundingLocked_opt.map(_.yourLast)
fundingTxId <- commitments1.active.find(_.localFundingStatus.isInstanceOf[LocalFundingStatus.Locked]).collect {
// Resend if the last splice_locked they received is different from our last locked commitment.
case c if c.fundingTxId != yourLast => c.fundingTxId
// Resend if we have not received their remote announcement_signatures for our latest locked commitment
case c if commitments1.announceChannel && d.lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId)) && c.fundingTxIndex > 0 &&
commitments1.latest.fundingTxId == yourLast => c.fundingTxId
}
} yield {
log.debug("re-sending splice_locked for fundingTxId={}", fundingTxId)
SpliceLocked(d.channelId, fundingTxId)
}

sendQueue = sendQueue ++ spliceLocked

// we may need to retransmit updates and/or commit_sig and/or revocation
sendQueue = sendQueue ++ syncSuccess.retransmit

// then we clean up unsigned updates
val commitments1 = d.commitments.discardUnsignedUpdates()
val commitments2 = commitments1.discardUnsignedUpdates()

commitments1.remoteNextCommitInfo match {
commitments2.remoteNextCommitInfo match {
case Left(_) =>
// we expect them to (re-)send the revocation immediately
startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout)
startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments2.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout)
case _ => ()
}

// do I have something to sign?
if (commitments1.changes.localHasChanges) {
if (commitments2.changes.localHasChanges) {
self ! CMD_SIGN()
}

Expand Down Expand Up @@ -2446,11 +2471,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with

// We tell the peer that the channel is ready to process payments that may be queued.
if (!shutdownInProgress) {
val fundingTxIndex = commitments1.active.map(_.fundingTxIndex).min
val fundingTxIndex = commitments2.active.map(_.fundingTxIndex).min
peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex)
}

goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1) sending sendQueue
goto(NORMAL) using d.copy(commitments = commitments2, spliceStatus = spliceStatus1) sending sendQueue
}

case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,19 @@ sealed trait ChannelReestablishTlv extends Tlv
object ChannelReestablishTlv {

case class NextFundingTlv(txId: TxId) extends ChannelReestablishTlv
case class LastFundingLockedTlv(yourLast: TxId, myCurrent: TxId) extends ChannelReestablishTlv

object NextFundingTlv {
val codec: Codec[NextFundingTlv] = tlvField(txIdAsHash)
}

object LastFundingLockedTlv {
val codec: Codec[LastFundingLockedTlv] = tlvField(("your_last_funding_locked_txid" | txIdAsHash) :: ("my_current_funding_locked_txid" | txIdAsHash))
}

val channelReestablishTlvCodec: Codec[TlvStream[ChannelReestablishTlv]] = tlvStream(discriminated[ChannelReestablishTlv].by(varint)
.typecase(UInt64(0), NextFundingTlv.codec)
.typecase(UInt64(1), LastFundingLockedTlv.codec)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ case class ChannelReestablish(channelId: ByteVector32,
myCurrentPerCommitmentPoint: PublicKey,
tlvStream: TlvStream[ChannelReestablishTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].map(_.txId)
val lastFundingLocked_opt: Option[ChannelReestablishTlv.LastFundingLockedTlv] = tlvStream.get[ChannelReestablishTlv.LastFundingLockedTlv]
}

case class OpenChannel(chainHash: BlockHash,
Expand Down
Loading

0 comments on commit 5cd1827

Please sign in to comment.