Skip to content

Commit 29628d5

Browse files
committed
Announce public splice transactions
We now support splicing on public channels: once the splice transaction is confirmed and locked on both sides, nodes will exchange announcement signatures that allows them to create a `channel_announcement` that they then broadcast to the network. This requires reworking the data model to include the announcement and the real `short_channel_id` in each commitment, which lets us cleanly distinguish real `short_channel_id`s from aliases (which are set at the channel level regardless of the current commitments). The flow now becomes: - when the funding transaction of a commitment confirms, we set the corresponding real `short_channel_id` in that commitment - if the channel is public and we've received `channel_ready` or `splice_locked`, we send our `announcement_signatures` - if we receive `announcement_signatures` for a commitment for which the funding transaction is unconfirmed, we stash it and replay it when the transaction confirms - if we receive `announcement_signatures` for a confirmed commitment, and we don't have a more recently announced commitment, we generate a `channel_announcement`, store it with the commitment and update our router data When creating a `channel_update` for a public channel, we always use the `short_channel_id` that matches the latest announcement we created. This is very important to guarantee that nodes receiving our updates will not discard them because they cannot match it to a channel. For private channels, we stop allowing usage of the `short_channel_id` for routing: `scid_alias` MUST be used, which ensures that the channel utxo isn't revealed. Note that when migrating to taproot channels, `splice_locked` will be used to transmit nonces for the announcement signatures, which will be compatible with the existing flow (and similarly, `channel_ready` will be used for the initial funding transaction). They are retransmitted on reconnection to ensure that the announcements can be generated.
1 parent 8c4f001 commit 29628d5

File tree

77 files changed

+1923
-617
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+1923
-617
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -417,23 +417,24 @@ case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Opti
417417
}
418418

419419
/**
420-
* Short identifiers for the channel.
420+
* Short identifiers for the channel that aren't related to the on-chain utxo.
421421
*
422-
* @param real_opt the real scid of the latest announced (and thus confirmed) funding transaction.
423422
* @param localAlias we must remember the alias that we sent to our peer because we use it to:
424423
* - identify incoming [[ChannelUpdate]] at the connection level
425424
* - route outgoing payments to that channel
426425
* @param remoteAlias_opt we only remember the last alias received from our peer, we use this to generate
427426
* routing hints in [[fr.acinq.eclair.payment.Bolt11Invoice]]
428427
*/
429-
case class ShortIds(real_opt: Option[RealShortChannelId], localAlias: Alias, remoteAlias_opt: Option[Alias])
428+
case class ShortIdAliases(localAlias: Alias, remoteAlias_opt: Option[Alias])
430429

431430
sealed trait LocalFundingStatus {
432431
def signedTx_opt: Option[Transaction]
433432
/** We store local signatures for the purpose of retransmitting if the funding/splicing flow is interrupted. */
434433
def localSigs_opt: Option[TxSignatures]
435434
/** Basic information about the liquidity purchase negotiated in this transaction, if any. */
436435
def liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]
436+
/** After confirmation, we store the channel announcement matching this funding transaction, once we've created it. */
437+
def announcement_opt: Option[ChannelAnnouncement]
437438
}
438439
object LocalFundingStatus {
439440
sealed trait NotLocked extends LocalFundingStatus
@@ -449,15 +450,18 @@ object LocalFundingStatus {
449450
case class SingleFundedUnconfirmedFundingTx(signedTx_opt: Option[Transaction]) extends UnconfirmedFundingTx with NotLocked {
450451
override val localSigs_opt: Option[TxSignatures] = None
451452
override val liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo] = None
453+
override val announcement_opt: Option[ChannelAnnouncement] = None
452454
}
453455
case class DualFundedUnconfirmedFundingTx(sharedTx: SignedSharedTransaction, createdAt: BlockHeight, fundingParams: InteractiveTxParams, liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends UnconfirmedFundingTx with NotLocked {
454456
override val signedTx_opt: Option[Transaction] = sharedTx.signedTx_opt
455457
override val localSigs_opt: Option[TxSignatures] = Some(sharedTx.localSigs)
458+
override val announcement_opt: Option[ChannelAnnouncement] = None
456459
}
457460
case class ZeroconfPublishedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends UnconfirmedFundingTx with Locked {
458461
override val signedTx_opt: Option[Transaction] = Some(tx)
462+
override val announcement_opt: Option[ChannelAnnouncement] = None
459463
}
460-
case class ConfirmedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends LocalFundingStatus with Locked {
464+
case class ConfirmedFundingTx(tx: Transaction, shortChannelId: RealShortChannelId, announcement_opt: Option[ChannelAnnouncement], localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends LocalFundingStatus with Locked {
461465
override val signedTx_opt: Option[Transaction] = Some(tx)
462466
}
463467
}
@@ -589,7 +593,7 @@ final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments,
589593
lastSent: Either[FundingCreated, FundingSigned]) extends ChannelDataWithCommitments {
590594
def fundingTx_opt: Option[Transaction] = commitments.latest.localFundingStatus.signedTx_opt
591595
}
592-
final case class DATA_WAIT_FOR_CHANNEL_READY(commitments: Commitments, shortIds: ShortIds) extends ChannelDataWithCommitments
596+
final case class DATA_WAIT_FOR_CHANNEL_READY(commitments: Commitments, aliases: ShortIdAliases) extends ChannelDataWithCommitments
593597

594598
final case class DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL_NON_INITIATOR) extends TransientChannelData {
595599
val channelId: ByteVector32 = init.temporaryChannelId
@@ -622,16 +626,17 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments,
622626
def latestFundingTx: DualFundedUnconfirmedFundingTx = commitments.latest.localFundingStatus.asInstanceOf[DualFundedUnconfirmedFundingTx]
623627
def previousFundingTxs: Seq[DualFundedUnconfirmedFundingTx] = allFundingTxs diff Seq(latestFundingTx)
624628
}
625-
final case class DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments: Commitments, shortIds: ShortIds) extends ChannelDataWithCommitments
629+
final case class DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments: Commitments, aliases: ShortIdAliases) extends ChannelDataWithCommitments
626630

627631
final case class DATA_NORMAL(commitments: Commitments,
628-
shortIds: ShortIds,
629-
channelAnnouncement: Option[ChannelAnnouncement],
632+
aliases: ShortIdAliases,
630633
channelUpdate: ChannelUpdate,
631634
localShutdown: Option[Shutdown],
632635
remoteShutdown: Option[Shutdown],
633636
closingFeerates: Option[ClosingFeerates],
634637
spliceStatus: SpliceStatus) extends ChannelDataWithCommitments {
638+
val lastAnnouncedCommitment_opt: Option[AnnouncedCommitment] = commitments.lastAnnouncement_opt
639+
val lastAnnouncement_opt: Option[ChannelAnnouncement] = lastAnnouncedCommitment_opt.map(_.announcement)
635640
val isNegotiatingQuiescence: Boolean = spliceStatus.isNegotiatingQuiescence
636641
val isQuiescent: Boolean = spliceStatus.isQuiescent
637642
}

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction, TxId}
2222
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
2323
import fr.acinq.eclair.channel.Helpers.Closing.ClosingType
2424
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, LiquidityAds}
25-
import fr.acinq.eclair.{BlockHeight, Features, MilliSatoshi, ShortChannelId}
25+
import fr.acinq.eclair.{BlockHeight, Features, MilliSatoshi, RealShortChannelId, ShortChannelId}
2626

2727
/**
2828
* Created by PM on 17/08/2016.
@@ -44,8 +44,8 @@ case class ChannelRestored(channel: ActorRef, channelId: ByteVector32, peer: Act
4444

4545
case class ChannelIdAssigned(channel: ActorRef, remoteNodeId: PublicKey, temporaryChannelId: ByteVector32, channelId: ByteVector32) extends ChannelEvent
4646

47-
/** This event will be sent whenever a new scid is assigned to the channel, be it a real, local alias or remote alias. */
48-
case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey, isAnnounced: Boolean) extends ChannelEvent
47+
/** This event will be sent whenever a new scid is assigned to the channel: local alias, remote alias or announcement. */
48+
case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, announcement_opt: Option[ChannelAnnouncement], aliases: ShortIdAliases, remoteNodeId: PublicKey) extends ChannelEvent
4949

5050
/** This event will be sent if a channel was aborted before completing the opening flow. */
5151
case class ChannelAborted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32) extends ChannelEvent
@@ -56,24 +56,24 @@ case class ChannelOpened(channel: ActorRef, remoteNodeId: PublicKey, channelId:
5656
/** This event is sent once channel_ready or splice_locked have been exchanged. */
5757
case class ChannelReadyForPayments(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, fundingTxIndex: Long) extends ChannelEvent
5858

59-
case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: Commitments) extends ChannelEvent {
59+
case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, aliases: ShortIdAliases, remoteNodeId: PublicKey, announcement_opt: Option[AnnouncedCommitment], channelUpdate: ChannelUpdate, commitments: Commitments) extends ChannelEvent {
6060
/**
6161
* We always include the local alias because we must always be able to route based on it.
6262
* However we only include the real scid if option_scid_alias is disabled, because we otherwise want to hide it.
6363
*/
6464
def scidsForRouting: Seq[ShortChannelId] = {
6565
val canUseRealScid = !commitments.params.channelFeatures.hasFeature(Features.ScidAlias)
6666
if (canUseRealScid) {
67-
shortIds.real_opt.toSeq :+ shortIds.localAlias
67+
announcement_opt.map(_.shortChannelId).toSeq :+ aliases.localAlias
6868
} else {
69-
Seq(shortIds.localAlias)
69+
Seq(aliases.localAlias)
7070
}
7171
}
7272
}
7373

7474
case class ChannelUpdateParametersChanged(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, channelUpdate: ChannelUpdate) extends ChannelEvent
7575

76-
case class LocalChannelDown(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey) extends ChannelEvent
76+
case class LocalChannelDown(channel: ActorRef, channelId: ByteVector32, realScids: Seq[RealShortChannelId], aliases: ShortIdAliases, remoteNodeId: PublicKey) extends ChannelEvent
7777

7878
case class ChannelStateChanged(channel: ActorRef, channelId: ByteVector32, peer: ActorRef, remoteNodeId: PublicKey, previousState: ChannelState, currentState: ChannelState, commitments_opt: Option[Commitments]) extends ChannelEvent
7979

@@ -97,7 +97,7 @@ case class TransactionPublished(channelId: ByteVector32, remoteNodeId: PublicKey
9797
case class TransactionConfirmed(channelId: ByteVector32, remoteNodeId: PublicKey, tx: Transaction) extends ChannelEvent
9898

9999
// NB: this event is only sent when the channel is available.
100-
case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, commitments: Commitments) extends ChannelEvent
100+
case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, aliases: ShortIdAliases, commitments: Commitments) extends ChannelEvent
101101

102102
case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: PersistentChannelData) extends ChannelEvent
103103

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.SharedTransaction
1212
import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager
1313
import fr.acinq.eclair.crypto.{Generators, ShaChain}
1414
import fr.acinq.eclair.payment.OutgoingPaymentPacket
15+
import fr.acinq.eclair.router.Announcements
1516
import fr.acinq.eclair.transactions.Transactions._
1617
import fr.acinq.eclair.transactions._
1718
import fr.acinq.eclair.wire.protocol._
18-
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, payment}
19+
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, payment}
1920
import scodec.bits.ByteVector
2021

2122
/** Static channel parameters shared by all commitments. */
@@ -278,6 +279,12 @@ case class Commitment(fundingTxIndex: Long,
278279
val commitInput: InputInfo = localCommit.commitTxAndRemoteSig.commitTx.input
279280
val fundingTxId: TxId = commitInput.outPoint.txid
280281
val capacity: Satoshi = commitInput.txOut.amount
282+
/** Once the funding transaction is confirmed, short_channel_id matching this transaction. */
283+
val shortChannelId_opt: Option[RealShortChannelId] = localFundingStatus match {
284+
case f: LocalFundingStatus.ConfirmedFundingTx => Some(f.shortChannelId)
285+
case _ => None
286+
}
287+
val announcement_opt: Option[ChannelAnnouncement] = localFundingStatus.announcement_opt
281288

282289
/** Channel reserve that applies to our funds. */
283290
def localChannelReserve(params: ChannelParams): Satoshi = params.localChannelReserveForCapacity(capacity, fundingTxIndex > 0)
@@ -367,6 +374,34 @@ case class Commitment(fundingTxIndex: Long,
367374
}
368375
}
369376

377+
/** Sign the announcement for this commitment, if the funding transaction is confirmed. */
378+
def signAnnouncement(nodeParams: NodeParams, params: ChannelParams): Option[AnnouncementSignatures] = {
379+
localFundingStatus match {
380+
case funding: LocalFundingStatus.ConfirmedFundingTx if params.announceChannel =>
381+
val features = Features.empty[Feature] // empty features for now
382+
val fundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex)
383+
val witness = Announcements.generateChannelAnnouncementWitness(
384+
nodeParams.chainHash,
385+
funding.shortChannelId,
386+
nodeParams.nodeKeyManager.nodeId,
387+
params.remoteParams.nodeId,
388+
fundingPubKey.publicKey,
389+
remoteFundingPubKey,
390+
features
391+
)
392+
val localBitcoinSig = nodeParams.channelKeyManager.signChannelAnnouncement(witness, fundingPubKey.path)
393+
val localNodeSig = nodeParams.nodeKeyManager.signChannelAnnouncement(witness)
394+
Some(AnnouncementSignatures(params.channelId, funding.shortChannelId, localNodeSig, localBitcoinSig))
395+
case _ => None
396+
}
397+
}
398+
399+
/** Add the channel_announcement provided if it is for this commitment. */
400+
def addAnnouncementIfMatches(ann: ChannelAnnouncement): Commitment = localFundingStatus match {
401+
case f: LocalFundingStatus.ConfirmedFundingTx if f.shortChannelId == ann.shortChannelId => copy(localFundingStatus = f.copy(announcement_opt = Some(ann)))
402+
case _ => this
403+
}
404+
370405
def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && nextRemoteCommit_opt.isEmpty
371406

372407
def hasNoPendingHtlcsOrFeeUpdate(changes: CommitmentChanges): Boolean = hasNoPendingHtlcs &&
@@ -732,6 +767,13 @@ object Commitment {
732767
}
733768
}
734769

770+
/** A commitment for which a channel announcement has been created. */
771+
case class AnnouncedCommitment(commitment: Commitment, announcement: ChannelAnnouncement) {
772+
val shortChannelId: RealShortChannelId = announcement.shortChannelId
773+
val fundingTxId: TxId = commitment.fundingTxId
774+
val fundingTxIndex: Long = commitment.fundingTxIndex
775+
}
776+
735777
/** Subset of Commitments when we want to work with a single, specific commitment. */
736778
case class FullCommitment(params: ChannelParams, changes: CommitmentChanges,
737779
fundingTxIndex: Long,
@@ -740,6 +782,10 @@ case class FullCommitment(params: ChannelParams, changes: CommitmentChanges,
740782
localFundingStatus: LocalFundingStatus, remoteFundingStatus: RemoteFundingStatus,
741783
localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) {
742784
val channelId = params.channelId
785+
val shortChannelId_opt = localFundingStatus match {
786+
case f: LocalFundingStatus.ConfirmedFundingTx => Some(f.shortChannelId)
787+
case _ => None
788+
}
743789
val localParams = params.localParams
744790
val remoteParams = params.remoteParams
745791
val commitInput = localCommit.commitTxAndRemoteSig.commitTx.input
@@ -810,13 +856,19 @@ case class Commitments(params: ChannelParams,
810856
lazy val availableBalanceForSend: MilliSatoshi = active.map(_.availableBalanceForSend(params, changes)).min
811857
lazy val availableBalanceForReceive: MilliSatoshi = active.map(_.availableBalanceForReceive(params, changes)).min
812858

859+
val all: Seq[Commitment] = active ++ inactive
860+
813861
// We always use the last commitment that was created, to make sure we never go back in time.
814862
val latest = FullCommitment(params, changes, active.head.fundingTxIndex, active.head.firstRemoteCommitIndex, active.head.remoteFundingPubKey, active.head.localFundingStatus, active.head.remoteFundingStatus, active.head.localCommit, active.head.remoteCommit, active.head.nextRemoteCommit_opt)
815-
816-
val all: Seq[Commitment] = active ++ inactive
863+
val lastAnnouncement_opt: Option[AnnouncedCommitment] = all.collectFirst { case c if c.announcement_opt.nonEmpty => AnnouncedCommitment(c, c.announcement_opt.get) }
817864

818865
def add(commitment: Commitment): Commitments = copy(active = commitment +: active)
819866

867+
def addAnnouncement(ann: ChannelAnnouncement): Commitments = copy(
868+
active = active.map(_.addAnnouncementIfMatches(ann)),
869+
inactive = inactive.map(_.addAnnouncementIfMatches(ann)),
870+
)
871+
820872
// @formatter:off
821873
def localIsQuiescent: Boolean = changes.localChanges.all.isEmpty
822874
def remoteIsQuiescent: Boolean = changes.remoteChanges.all.isEmpty
@@ -1155,7 +1207,7 @@ case class Commitments(params: ChannelParams,
11551207
def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = {
11561208
all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.localSigs_opt)
11571209
}
1158-
1210+
11591211
def liquidityPurchase(fundingTxId: TxId): Option[LiquidityAds.PurchaseBasicInfo] = {
11601212
all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.liquidityPurchase_opt)
11611213
}
@@ -1248,10 +1300,19 @@ case class Commitments(params: ChannelParams,
12481300
.sortBy(_.fundingTxIndex)
12491301
.lastOption match {
12501302
case Some(lastConfirmed) =>
1251-
// We can prune all other commitments with the same or lower funding index.
12521303
// NB: we cannot prune active commitments, even if we know that they have been double-spent, because our peer
12531304
// may not yet be aware of it, and will expect us to send commit_sig.
1254-
val pruned = inactive.filter(c => c.fundingTxId != lastConfirmed.fundingTxId && c.fundingTxIndex <= lastConfirmed.fundingTxIndex)
1305+
val pruned = if (params.announceChannel) {
1306+
// If the most recently confirmed commitment isn't announced yet, we cannot prune the last commitment we
1307+
// announced, because our channel updates are based on its announcement (and its short_channel_id).
1308+
// If we never announced the channel, we don't need to announce old commitments, we will directly announce the last one.
1309+
val pruningIndex = lastAnnouncement_opt.map(_.fundingTxIndex).getOrElse(lastConfirmed.fundingTxIndex)
1310+
// We can prune all RBF candidates, and commitments that came before the last announced one.
1311+
inactive.filter(c => c.fundingTxIndex < pruningIndex || (c.fundingTxIndex == lastConfirmed.fundingTxIndex && c.fundingTxId != lastConfirmed.fundingTxId))
1312+
} else {
1313+
// We can prune all other commitments with the same or lower funding index.
1314+
inactive.filter(c => c.fundingTxId != lastConfirmed.fundingTxId && c.fundingTxIndex <= lastConfirmed.fundingTxIndex)
1315+
}
12551316
pruned.foreach(c => log.info("pruning commitment fundingTxIndex={} fundingTxId={}", c.fundingTxIndex, c.fundingTxId))
12561317
copy(inactive = inactive diff pruned)
12571318
case _ =>
@@ -1267,6 +1328,14 @@ case class Commitments(params: ChannelParams,
12671328
def resolveCommitment(spendingTx: Transaction): Option[Commitment] = {
12681329
all.find(c => spendingTx.txIn.map(_.outPoint).contains(c.commitInput.outPoint))
12691330
}
1331+
1332+
/** Find the corresponding commitment based on its short_channel_id (once funding transaction is confirmed). */
1333+
def resolveCommitment(shortChannelId: RealShortChannelId): Option[Commitment] = {
1334+
all.find(c => c.localFundingStatus match {
1335+
case f: LocalFundingStatus.ConfirmedFundingTx => f.shortChannelId == shortChannelId
1336+
case _ => false
1337+
})
1338+
}
12701339
}
12711340

12721341
object Commitments {

0 commit comments

Comments
 (0)