Skip to content

Commit 090dcbe

Browse files
committed
Re-work my_current_funding_locked reestablish
We introduce a `retransmit_flags` field to ask our peer to retransmit `announcement_signatures` if we're expecting them: this way we don't need to retransmit `splice_locked` on reconnection to trigger the exchange of `announcement_signatures`. We don't need to retransmit it to let our peer know that we've seen enough confirmations for the splice either, since `my_current_funding_locked` implies that. This allows us to completely remove retransmission of `splice_locked` on reconnection, and also get rid of the `your_last_funding_locked` TLV, which greatly simplifies the reconnection logic. This will work with taproot channels since we'll simply provide nonces in `channel_reestablish` when we need our peer to send announcement signatures. Note that we use a different TLV type to more easily allow migrating existing users of the previous versions of the spec to the latest one.
1 parent b440de4 commit 090dcbe

File tree

9 files changed

+131
-242
lines changed

9 files changed

+131
-242
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 22 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,6 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
236236
var announcementSigsStash = Map.empty[RealShortChannelId, AnnouncementSignatures]
237237
// we record the announcement_signatures messages we already sent to avoid unnecessary retransmission
238238
var announcementSigsSent = Set.empty[RealShortChannelId]
239-
// we keep track of the splice_locked we sent after channel_reestablish and it's funding tx index to avoid sending it again
240-
private var spliceLockedSent = Map.empty[TxId, Long]
241239

242240
private def trimAnnouncementSigsStashIfNeeded(): Unit = {
243241
if (announcementSigsStash.size >= 10) {
@@ -249,17 +247,6 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
249247
}
250248
}
251249

252-
private def trimSpliceLockedSentIfNeeded(): Unit = {
253-
if (spliceLockedSent.size >= 10) {
254-
// We shouldn't store an unbounded number of splice_locked: on long-lived connections where we do a lot of splice
255-
// transactions, we only need to keep track of the most recent ones.
256-
val oldestFundingTxId = spliceLockedSent.toSeq
257-
.sortBy { case (_, fundingTxIndex) => fundingTxIndex }
258-
.map { case (fundingTxId, _) => fundingTxId }.head
259-
spliceLockedSent -= oldestFundingTxId
260-
}
261-
}
262-
263250
val txPublisher = txPublisherFactory.spawnTxPublisher(context, remoteNodeId)
264251

265252
// this will be used to detect htlc timeouts
@@ -1427,8 +1414,6 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
14271414
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepth), delay_opt = None)
14281415
maybeEmitEventsPostSplice(d.aliases, d.commitments, commitments1, d.lastAnnouncement_opt)
14291416
maybeUpdateMaxHtlcAmount(d.channelUpdate.htlcMaximumMsat, commitments1)
1430-
spliceLockedSent += (commitment.fundingTxId -> commitment.fundingTxIndex)
1431-
trimSpliceLockedSentIfNeeded()
14321417
stay() using d.copy(commitments = commitments1) storing() sending SpliceLocked(d.channelId, w.tx.txid)
14331418
case Left(_) => stay()
14341419
}
@@ -1439,11 +1424,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
14391424
// We check if this commitment was already locked before receiving the event (which happens when using 0-conf
14401425
// or for the initial funding transaction). If it was previously not locked, we must send splice_locked now.
14411426
val previouslyNotLocked = d.commitments.all.exists(c => c.fundingTxId == commitment.fundingTxId && c.localFundingStatus.isInstanceOf[LocalFundingStatus.NotLocked])
1442-
val spliceLocked_opt = if (previouslyNotLocked) {
1443-
spliceLockedSent += (commitment.fundingTxId -> commitment.fundingTxIndex)
1444-
trimSpliceLockedSentIfNeeded()
1445-
Some(SpliceLocked(d.channelId, w.tx.txid))
1446-
} else None
1427+
val spliceLocked_opt = if (previouslyNotLocked) Some(SpliceLocked(d.channelId, w.tx.txid)) else None
14471428
// If the channel is public and we've received the remote splice_locked, we send our announcement_signatures
14481429
// in order to generate the channel_announcement.
14491430
val remoteLocked = commitment.fundingTxIndex == 0 || d.commitments.all.exists(c => c.fundingTxId == commitment.fundingTxId && c.remoteFundingStatus == RemoteFundingStatus.Locked)
@@ -1466,21 +1447,6 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
14661447
case Event(msg: SpliceLocked, d: DATA_NORMAL) =>
14671448
d.commitments.updateRemoteFundingStatus(msg.fundingTxId, d.lastAnnouncedFundingTxId_opt) match {
14681449
case Right((commitments1, commitment)) =>
1469-
// If we have both already sent splice_locked for this commitment, then we are receiving splice_locked
1470-
// again after a reconnection and must retransmit our splice_locked and new announcement_signatures. Nodes
1471-
// retransmit splice_locked after a reconnection when they have received splice_locked but NOT matching signatures
1472-
// before the last disconnect. If a matching splice_locked has already been sent since reconnecting, then do not
1473-
// retransmit splice_locked to avoid a loop.
1474-
// NB: It is important both nodes retransmit splice_locked after reconnecting to ensure new Taproot nonces
1475-
// are exchanged for channel announcements.
1476-
val isLatestLocked = d.commitments.lastLocalLocked_opt.exists(_.fundingTxId == msg.fundingTxId) && d.commitments.lastRemoteLocked_opt.exists(_.fundingTxId == msg.fundingTxId)
1477-
val spliceLocked_opt = if (d.commitments.announceChannel && isLatestLocked && !spliceLockedSent.contains(commitment.fundingTxId)) {
1478-
spliceLockedSent += (commitment.fundingTxId -> commitment.fundingTxIndex)
1479-
trimSpliceLockedSentIfNeeded()
1480-
Some(SpliceLocked(d.channelId, commitment.fundingTxId))
1481-
} else {
1482-
None
1483-
}
14841450
// If the commitment is confirmed, we were waiting to receive the remote splice_locked before sending our announcement_signatures.
14851451
val localAnnSigs_opt = commitment.signAnnouncement(nodeParams, commitments1.channelParams, channelKeys.fundingKey(commitment.fundingTxIndex)) match {
14861452
case Some(localAnnSigs) if !announcementSigsSent.contains(localAnnSigs.shortChannelId) =>
@@ -1493,7 +1459,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
14931459
}
14941460
maybeEmitEventsPostSplice(d.aliases, d.commitments, commitments1, d.lastAnnouncement_opt)
14951461
maybeUpdateMaxHtlcAmount(d.channelUpdate.htlcMaximumMsat, commitments1)
1496-
stay() using d.copy(commitments = commitments1) storing() sending spliceLocked_opt.toSeq ++ localAnnSigs_opt.toSeq
1462+
stay() using d.copy(commitments = commitments1) storing() sending localAnnSigs_opt.toSeq
14971463
case Left(_) => stay()
14981464
}
14991465

@@ -2394,8 +2360,14 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
23942360
}
23952361
val remoteFeatures = d.commitments.remoteChannelParams.initFeatures
23962362
val lastFundingLockedTlvs: Set[ChannelReestablishTlv] = if (remoteFeatures.hasFeature(Features.Splicing) || remoteFeatures.unknown.contains(UnknownFeature(154)) || remoteFeatures.unknown.contains(UnknownFeature(155))) {
2397-
d.commitments.lastLocalLocked_opt.map(c => ChannelReestablishTlv.MyCurrentFundingLockedTlv(c.fundingTxId)).toSet ++
2398-
d.commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv.YourLastFundingLockedTlv(c.fundingTxId)).toSet
2363+
d.commitments.lastLocalLocked_opt.map(c => {
2364+
// We ask our peer to retransmit their announcement_signatures if we haven't already announced that splice.
2365+
val retransmitAnnSigs = d match {
2366+
case d: DATA_NORMAL if d.commitments.announceChannel => !d.lastAnnouncedFundingTxId_opt.contains(c.fundingTxId)
2367+
case _ => false
2368+
}
2369+
ChannelReestablishTlv.MyCurrentFundingLockedTlv(c.fundingTxId, retransmitAnnSigs)
2370+
}).toSet
23992371
} else Set.empty
24002372

24012373
val channelReestablish = ChannelReestablish(
@@ -2522,22 +2494,18 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
25222494
// We only send channel_ready for initial funding transactions.
25232495
case Some(c) if c.fundingTxIndex != 0 => ()
25242496
case Some(c) =>
2525-
val remoteFeatures = d.commitments.remoteChannelParams.initFeatures
2526-
val remoteSpliceSupport = remoteFeatures.hasFeature(Features.Splicing) || remoteFeatures.unknown.contains(UnknownFeature(154)) || remoteFeatures.unknown.contains(UnknownFeature(155))
2527-
// If our peer has not received our channel_ready, we retransmit it.
2528-
val notReceivedByRemote = remoteSpliceSupport && channelReestablish.yourLastFundingLocked_opt.isEmpty
25292497
// If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node
25302498
// MUST retransmit channel_ready, otherwise it MUST NOT
2531-
val notReceivedByRemoteLegacy = !remoteSpliceSupport && channelReestablish.nextLocalCommitmentNumber == 1 && c.localCommit.index == 0
2499+
val notReceivedByRemote = channelReestablish.nextLocalCommitmentNumber == 1 && c.localCommit.index == 0
25322500
// If this is a public channel and we haven't announced the channel, we retransmit our channel_ready and
25332501
// will also send announcement_signatures.
25342502
val notAnnouncedYet = d.commitments.announceChannel && c.shortChannelId_opt.nonEmpty && d.lastAnnouncement_opt.isEmpty
2535-
if (notAnnouncedYet || notReceivedByRemote || notReceivedByRemoteLegacy) {
2503+
if (notAnnouncedYet || notReceivedByRemote) {
25362504
log.debug("re-sending channel_ready")
25372505
val nextPerCommitmentPoint = channelKeys.commitmentPoint(1)
25382506
sendQueue = sendQueue :+ ChannelReady(d.commitments.channelId, nextPerCommitmentPoint)
25392507
}
2540-
if (notAnnouncedYet) {
2508+
if (notAnnouncedYet || channelReestablish.retransmitAnnSigs) {
25412509
// The funding transaction is confirmed, so we've already sent our announcement_signatures.
25422510
// We haven't announced the channel yet, which means we haven't received our peer's announcement_signatures.
25432511
// We retransmit our announcement_signatures to let our peer know that we're ready to announce the channel.
@@ -2600,23 +2568,19 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
26002568
// We then clean up unsigned updates that haven't been received before the disconnection.
26012569
.discardUnsignedUpdates()
26022570

2571+
// We retransmit our latest announcement_signatures if our peer requests it.
26032572
commitments1.lastLocalLocked_opt match {
26042573
case None => ()
2605-
// We only send splice_locked for splice transactions.
2574+
// This retransmit mechanism is only available for splice transactions.
26062575
case Some(c) if c.fundingTxIndex == 0 => ()
26072576
case Some(c) =>
2608-
// If our peer has not received our splice_locked, we retransmit it.
2609-
val notReceivedByRemote = !channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId)
2610-
// If this is a public channel and we haven't announced the splice, we retransmit our splice_locked and
2611-
// will exchange announcement_signatures afterwards.
2612-
val notAnnouncedYet = commitments1.announceChannel && d.lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId))
2613-
if (notReceivedByRemote || notAnnouncedYet) {
2614-
// Retransmission of local announcement_signatures for splices are done when receiving splice_locked, no need
2615-
// to retransmit here.
2616-
log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId)
2617-
spliceLockedSent += (c.fundingTxId -> c.fundingTxIndex)
2618-
trimSpliceLockedSentIfNeeded()
2619-
sendQueue = sendQueue :+ SpliceLocked(d.channelId, c.fundingTxId)
2577+
if (channelReestablish.retransmitAnnSigs && d.commitments.announceChannel) {
2578+
val localAnnSigs = c.signAnnouncement(nodeParams, d.commitments.channelParams, channelKeys.fundingKey(c.fundingTxIndex))
2579+
localAnnSigs.foreach(annSigs => {
2580+
log.debug("re-sending announcement_signatures for fundingTxId={}", c.fundingTxId)
2581+
announcementSigsSent += annSigs.shortChannelId
2582+
sendQueue = sendQueue :+ annSigs
2583+
})
26202584
}
26212585
}
26222586

@@ -3108,7 +3072,6 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
31083072
case _ -> OFFLINE =>
31093073
announcementSigsStash = Map.empty
31103074
announcementSigsSent = Set.empty
3111-
spliceLockedSent = Map.empty[TxId, Long]
31123075
}
31133076

31143077
/*

eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,13 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
436436
}
437437
// If our peer is using the experimental splice version, we convert splice messages.
438438
case msg: ExperimentalSpliceInit =>
439-
d.peer ! msg.toSpliceInit()
439+
d.peer ! msg.toSpliceInit
440440
stay()
441441
case msg: ExperimentalSpliceAck =>
442-
d.peer ! msg.toSpliceAck()
442+
d.peer ! msg.toSpliceAck
443443
stay()
444444
case msg: ExperimentalSpliceLocked =>
445-
d.peer ! msg.toSpliceLocked()
445+
d.peer ! msg.toSpliceLocked
446446
stay()
447447
case _ =>
448448
d.peer ! msg

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -240,28 +240,23 @@ object ChannelReestablishTlv {
240240
*/
241241
case class NextFundingTlv(txId: TxId) extends ChannelReestablishTlv
242242

243-
/** The txid of the last [[ChannelReady]] or [[SpliceLocked]] message received before disconnecting, if any. */
244-
case class YourLastFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
245-
246-
/** The txid of our latest outgoing [[ChannelReady]] or [[SpliceLocked]] for this channel. */
247-
case class MyCurrentFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
243+
/**
244+
* @param txId the txid of our latest outgoing [[ChannelReady]] or [[SpliceLocked]] for this channel.
245+
* @param retransmitAnnSigs true if [[AnnouncementSignatures]] must be retransmitted.
246+
*/
247+
case class MyCurrentFundingLockedTlv(txId: TxId, retransmitAnnSigs: Boolean) extends ChannelReestablishTlv
248248

249249
object NextFundingTlv {
250250
val codec: Codec[NextFundingTlv] = tlvField(txIdAsHash)
251251
}
252252

253-
object YourLastFundingLockedTlv {
254-
val codec: Codec[YourLastFundingLockedTlv] = tlvField("your_last_funding_locked_txid" | txIdAsHash)
255-
}
256-
257253
object MyCurrentFundingLockedTlv {
258-
val codec: Codec[MyCurrentFundingLockedTlv] = tlvField("my_current_funding_locked_txid" | txIdAsHash)
254+
val codec: Codec[MyCurrentFundingLockedTlv] = tlvField(("my_current_funding_locked_txid" | txIdAsHash) :: ("retransmit_flags" | (ignore(7) :: bool)))
259255
}
260256

261257
val channelReestablishTlvCodec: Codec[TlvStream[ChannelReestablishTlv]] = tlvStream(discriminated[ChannelReestablishTlv].by(varint)
262258
.typecase(UInt64(0), NextFundingTlv.codec)
263-
.typecase(UInt64(1), YourLastFundingLockedTlv.codec)
264-
.typecase(UInt64(3), MyCurrentFundingLockedTlv.codec)
259+
.typecase(UInt64(5), MyCurrentFundingLockedTlv.codec)
265260
)
266261
}
267262

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ case class ChannelReestablish(channelId: ByteVector32,
196196
tlvStream: TlvStream[ChannelReestablishTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
197197
val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].map(_.txId)
198198
val myCurrentFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].map(_.txId)
199-
val yourLastFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.YourLastFundingLockedTlv].map(_.txId)
199+
val retransmitAnnSigs: Boolean = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].exists(_.retransmitAnnSigs)
200200
}
201201

202202
case class OpenChannel(chainHash: BlockHash,
@@ -343,7 +343,7 @@ case class ExperimentalSpliceInit(channelId: ByteVector32,
343343
lockTime: Long,
344344
fundingPubKey: PublicKey,
345345
tlvStream: TlvStream[SpliceInitTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
346-
def toSpliceInit(): SpliceInit = SpliceInit(channelId, fundingContribution, feerate, lockTime, fundingPubKey, tlvStream)
346+
def toSpliceInit: SpliceInit = SpliceInit(channelId, fundingContribution, feerate, lockTime, fundingPubKey, tlvStream)
347347
}
348348

349349
object ExperimentalSpliceInit {
@@ -375,7 +375,7 @@ case class ExperimentalSpliceAck(channelId: ByteVector32,
375375
fundingContribution: Satoshi,
376376
fundingPubKey: PublicKey,
377377
tlvStream: TlvStream[SpliceAckTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
378-
def toSpliceAck(): SpliceAck = SpliceAck(channelId, fundingContribution, fundingPubKey, tlvStream)
378+
def toSpliceAck: SpliceAck = SpliceAck(channelId, fundingContribution, fundingPubKey, tlvStream)
379379
}
380380

381381
object ExperimentalSpliceAck {
@@ -390,7 +390,7 @@ case class SpliceLocked(channelId: ByteVector32,
390390
case class ExperimentalSpliceLocked(channelId: ByteVector32,
391391
fundingTxId: TxId,
392392
tlvStream: TlvStream[SpliceLockedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
393-
def toSpliceLocked(): SpliceLocked = SpliceLocked(channelId, fundingTxId, tlvStream)
393+
def toSpliceLocked: SpliceLocked = SpliceLocked(channelId, fundingTxId, tlvStream)
394394
}
395395

396396
object ExperimentalSpliceLocked {

eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import fr.acinq.eclair.channel.fsm.Channel
1212
import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory
1313
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
1414
import fr.acinq.eclair.router.Announcements
15-
import fr.acinq.eclair.wire.protocol.{ChannelReestablish, ChannelUpdate, CommitSig, Error, Init, RevokeAndAck}
15+
import fr.acinq.eclair.wire.protocol.{ChannelReady, ChannelReestablish, ChannelUpdate, CommitSig, Error, Init, RevokeAndAck}
1616
import fr.acinq.eclair.{TestKitBaseClass, _}
1717
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
1818
import org.scalatest.{Outcome, Tag}
@@ -197,14 +197,15 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan
197197
assert(u1.channelUpdate.feeProportionalMillionths == newConfig.relayParams.privateChannelFees.feeProportionalMillionths)
198198
assert(u1.channelUpdate.cltvExpiryDelta == newConfig.channelConf.expiryDelta)
199199

200+
alice2bob.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
201+
bob2alice.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
202+
200203
newAlice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)
201204
bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)
202205
alice2bob.expectMsgType[ChannelReestablish]
203206
bob2alice.expectMsgType[ChannelReestablish]
204207
alice2bob.forward(bob)
205208
bob2alice.forward(newAlice)
206-
alice2bob.expectMsgType[ChannelUpdate]
207-
bob2alice.expectMsgType[ChannelUpdate]
208209
alice2bob.expectNoMessage()
209210
bob2alice.expectNoMessage()
210211

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,18 +238,16 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF
238238
val bobInit = Init(TestConstants.Bob.nodeParams.features.initFeatures())
239239
alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)
240240
bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)
241-
alice2bob.expectMsgType[ChannelReestablish]
241+
assert(alice2bob.expectMsgType[ChannelReestablish].retransmitAnnSigs)
242242
alice2bob.forward(bob)
243-
bob2alice.expectMsgType[ChannelReestablish]
243+
assert(!bob2alice.expectMsgType[ChannelReestablish].retransmitAnnSigs)
244244
bob2alice.forward(alice)
245-
// Bob does not retransmit channel_ready and announcement_signatures because he has already received both of them from Alice.
246-
bob2alice.expectNoMessage(100 millis)
247-
// Alice has already received Bob's channel_ready, but not its announcement_signatures.
248-
// She retransmits channel_ready and Bob will retransmit its announcement_signatures in response.
249245
alice2bob.expectMsgType[ChannelReady]
250246
alice2bob.forward(bob)
251247
alice2bob.expectMsgType[AnnouncementSignatures]
252248
alice2bob.forward(bob)
249+
bob2alice.expectMsgType[ChannelReady]
250+
bob2alice.forward(alice)
253251
bob2alice.expectMsgType[AnnouncementSignatures]
254252
bob2alice.forward(alice)
255253
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.nonEmpty)

0 commit comments

Comments
 (0)