Add support for the official splicing protocol (#2887)
What changed, and why it matters
This commit adds official Lightning channel splicing support to Eclair and replaces an experimental protocol. It includes backwards-compatibility shims for older Phoenix wallets, changes how reconnection messages request retransmission of signatures, and renumbers some protocol feature bits. The changes are broad and touch consensus-critical wire protocol and channel state logic, so bugs could affect channel safety, but the commit itself is a feature implementation rather than a clear security fix.
Treat this as a high-risk feature change requiring careful review and extensive interoperability testing, especially around reconnection edge cases, legacy/experimental peer compatibility, and signature retransmission. Run the updated test suite and consider fuzzing the new TLV codecs and state transitions. No immediate emergency patch is indicated from the diff alone.
Security signals we found
Protocol state machine changes for interactive transaction signing and reconnection retransmission
Backwards-compatibility shim that multiplexes two protocol versions based on peer feature bits
TLV type-number collision handling between official and experimental splicing protocols
Removal of `splice_locked` retransmission path for non-legacy peers, replaced by explicit retransmit flags
Changes to commitment signature retransmission logic on reconnection
Evidence from the diff
The patch implements BOLT splicing (bolts PR 1160) in Eclair. It introduces the option_splice feature bit (62), renames/replaces experimental SplicePrototype usage, adds official message types splice_init (80), splice_ack (81), splice_locked (77), and converts between official and experimental encodings at the peer-connection layer. Reconnection TLVs are refactored: NextFundingTlv is replaced by a combined NextFundingOrExperimentalYourLastFundingLockedTlv carrying a txid plus a retransmit-commit-sig flag, and MyCurrentFundingLockedTlv now carries a retransmit-announcement-signatures flag. The code stops retransmitting splice_locked on reconnection for official-protocol peers, relying instead on my_current_funding_locked. Legacy Phoenix peers still trigger old behavior via useLegacySpliceProtocol. Tests are updated to assert new retransmit flags and message flows.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/Features.scalaeclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scalaeclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scalaeclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scalaeclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scalaeclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scalaeclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scalaeclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scalaInspect captured patch +867 / −305
diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md
index 74a3dd4..85b01d1 100644
--- a/docs/release-notes/eclair-vnext.md
+++ b/docs/release-notes/eclair-vnext.md
@@ -9,6 +9,39 @@
With this release, eclair requires using Bitcoin Core 30.x.
Newer versions of Bitcoin Core may be used, but have not been extensively tested.
+### Channel Splicing
+
+With this release, we add support for the final version of [splicing](https://github.com/lightning/bolts/pull/1160) that was recently added to the BOLTs.
+Splicing allows node operators to change the size of their existing channels, which makes it easier and more efficient to allocate liquidity where it is most needed.
+Most node operators can now have a single channel with each of their peer, which costs less on-chain fees and resources, and makes path-finding easier.
+
+The size of an existing channel can be increased with the `splicein` API:
+
+```sh
+eclair-cli splicein --channelId=<channel_id> --amountIn=<amount_satoshis>
+```
+
+Once that transaction confirms, the additional liquidity can be used to send outgoing payments.
+If the transaction doesn't confirm, the node operator can speed up confirmation with the `rbfsplice` API:
+
+```sh
+eclair-cli rbfsplice --channelId=<channel_id> --targetFeerateSatByte=<feerate_satoshis_per_byte> --fundingFeeBudgetSatoshis=<maximum_on_chain_fee_satoshis>
+```
+
+If the node operator wants to reduce the size of a channel, or send some of the channel funds to an on-chain address, they can use the `spliceout` API:
+
+```sh
+eclair-cli spliceout --channelId=<channel_id> --amountOut=<amount_satoshis> --scriptPubKey=<on_chain_address>
+```
+
+That operation can also be RBF-ed with the `rbfsplice` API to speed up confirmation if necessary.
+
+Note that when 0-conf is used for the channel, it is not possible to RBF splice transactions.
+Node operators should instead create a new splice transaction (with `splicein` or `spliceout`) to CPFP the previous transaction.
+
+Note that eclair had already introduced support for a splicing prototype in v0.9.0, which helped improve the BOLT proposal.
+We're removing support for the previous splicing prototype feature: users that depended on this protocol must upgrade to create official splice transactions.
+
### Remove support for non-anchor channels
We remove the code used to support legacy channels that don't use anchor outputs or taproot.
diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf
index 757f1ec..fb90bf4 100644
--- a/eclair-core/src/main/resources/reference.conf
+++ b/eclair-core/src/main/resources/reference.conf
@@ -88,6 +88,7 @@ eclair {
option_zeroconf = disabled
keysend = disabled
option_simple_close = optional
+ option_splice = optional
trampoline_payment_prototype = disabled
async_payment_prototype = disabled
on_the_fly_funding = disabled
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
index 3035c24..2325bea 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
@@ -344,8 +344,7 @@ object Features {
val mandatory = 28
}
- // TODO: this should also extend NodeFeature once the spec is finalized
- case object Quiescence extends Feature with InitFeature {
+ case object Quiescence extends Feature with InitFeature with NodeFeature {
val rfcName = "option_quiesce"
val mandatory = 34
}
@@ -395,6 +394,11 @@ object Features {
val mandatory = 60
}
+ case object Splicing extends Feature with InitFeature with NodeFeature {
+ val rfcName = "option_splice"
+ val mandatory = 62
+ }
+
case object PhoenixZeroReserve extends Feature with InitFeature with ChannelTypeFeature with PermanentChannelFeature {
val rfcName = "phoenix_zero_reserve"
val mandatory = 128
@@ -482,6 +486,7 @@ object Features {
ZeroConf,
KeySend,
SimpleClose,
+ Splicing,
SimpleTaprootChannelsPhoenix,
SimpleTaprootChannelsStaging,
WakeUpNotificationClient,
@@ -506,7 +511,6 @@ object Features {
SimpleClose -> (ShutdownAnySegwit :: Nil),
SimpleTaprootChannelsPhoenix -> (ChannelType :: SimpleClose :: Nil),
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
- OnTheFlyFunding -> (SplicePrototype :: Nil),
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala
index dd18fca..5a630f5 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala
@@ -33,6 +33,8 @@ case class ChannelParams(channelId: ByteVector32,
val remoteNodeId: PublicKey = remoteParams.nodeId
// If we've set the 0-conf feature bit for this peer, we will always use 0-conf with them.
val zeroConf: Boolean = localParams.initFeatures.hasFeature(Features.ZeroConf)
+ // TODO: we keep supporting the legacy splicing protocol for non-upgraded Phoenix users.
+ lazy val useLegacySpliceProtocol = remoteParams.initFeatures.hasFeature(Features.SplicePrototype)
/** We update local/global features at reconnection. */
def updateFeatures(localInit: Init, remoteInit: Init): ChannelParams = copy(
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
index 172f5bb..407f0cb 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
@@ -247,6 +247,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
// we record the announcement_signatures messages we already sent to avoid unnecessary retransmission
var announcementSigsSent = Set.empty[RealShortChannelId]
// we keep track of the splice_locked we sent after channel_reestablish and it's funding tx index to avoid sending it again
+ // TODO: we can remove that once we stop supporting the legacy splicing protocol
private var spliceLockedSent = Map.empty[TxId, Long]
private def trimAnnouncementSigsStashIfNeeded(): Unit = {
@@ -955,7 +956,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
}
case Event(cmd: CMD_SPLICE, d: DATA_NORMAL) =>
- if (!d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype)) {
+ if (!d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.Splicing) && !d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype)) {
log.warning("cannot initiate splice, peer doesn't support splicing")
cmd.replyTo ! RES_FAILURE(cmd, CommandUnavailableInThisState(d.channelId, "splice", NORMAL))
stay()
@@ -1519,18 +1520,22 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Event(msg: SpliceLocked, d: DATA_NORMAL) =>
d.commitments.updateRemoteFundingStatus(msg.fundingTxId, d.lastAnnouncedFundingTxId_opt) match {
case Right((commitments1, commitment)) =>
- // If we have both already sent splice_locked 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
- // before the last disconnect. If a matching splice_locked has already been sent since reconnecting, then do not
- // retransmit splice_locked to avoid a loop.
- // NB: It is important both nodes retransmit splice_locked after reconnecting to ensure new Taproot nonces
- // are exchanged for channel announcements.
- val isLatestLocked = d.commitments.lastLocalLocked_opt.exists(_.fundingTxId == msg.fundingTxId) && d.commitments.lastRemoteLocked_opt.exists(_.fundingTxId == msg.fundingTxId)
- val spliceLocked_opt = if (d.commitments.announceChannel && isLatestLocked && !spliceLockedSent.contains(commitment.fundingTxId)) {
- spliceLockedSent += (commitment.fundingTxId -> commitment.fundingTxIndex)
- trimSpliceLockedSentIfNeeded()
- Some(SpliceLocked(d.channelId, commitment.fundingTxId))
+ val spliceLocked_opt = if (d.channelParams.useLegacySpliceProtocol) {
+ // If we have both already sent splice_locked 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
+ // before the last disconnect. If a matching splice_locked has already been sent since reconnecting, then do not
+ // retransmit splice_locked to avoid a loop.
+ // NB: It is important both nodes retransmit splice_locked after reconnecting to ensure new Taproot nonces
+ // are exchanged for channel announcements.
+ val isLatestLocked = d.commitments.lastLocalLocked_opt.exists(_.fundingTxId == msg.fundingTxId) && d.commitments.lastRemoteLocked_opt.exists(_.fundingTxId == msg.fundingTxId)
+ if (d.commitments.announceChannel && isLatestLocked && !spliceLockedSent.contains(commitment.fundingTxId)) {
+ spliceLockedSent += (commitment.fundingTxId -> commitment.fundingTxIndex)
+ trimSpliceLockedSentIfNeeded()
+ Some(SpliceLocked(d.channelId, commitment.fundingTxId))
+ } else {
+ None
+ }
} else {
None
}
@@ -2412,7 +2417,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Event(INPUT_RECONNECTED(r, localInit, remoteInit), d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) =>
activeConnection = r
val myFirstPerCommitmentPoint = channelKeys.commitmentPoint(0)
- val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTxId))
+ val nextFundingTlv = if (d.channelParams.useLegacySpliceProtocol) {
+ Set[ChannelReestablishTlv](ChannelReestablishTlv.ExperimentalNextFundingTlv(d.signingSession.fundingTxId))
+ } else {
+ Set[ChannelReestablishTlv](ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv.asNextFunding(d.signingSession.fundingTxId, d.signingSession.retransmitRemoteCommitSig))
+ }
val nonceTlvs = d.signingSession.fundingParams.commitmentFormat match {
case _: SegwitV0CommitmentFormat => Set.empty
case _: SimpleTaprootChannelCommitmentFormat =>
@@ -2430,7 +2439,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
}
val channelReestablish = ChannelReestablish(
channelId = d.channelId,
- nextLocalCommitmentNumber = d.signingSession.nextLocalCommitmentNumber,
+ nextLocalCommitmentNumber = d.signingSession.nextLocalCommitmentNumber(d.channelParams.useLegacySpliceProtocol),
nextRemoteRevocationNumber = 0,
yourLastPerCommitmentSecret = PrivateKey(ByteVector32.Zeroes),
myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint,
@@ -2444,40 +2453,72 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
val remotePerCommitmentSecrets = d.commitments.remotePerCommitmentSecrets
val yourLastPerCommitmentSecret = remotePerCommitmentSecrets.lastIndex.flatMap(remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes)
val myCurrentPerCommitmentPoint = channelKeys.commitmentPoint(d.commitments.localCommitIndex)
- // If we disconnected while signing a funding transaction, we may need our peer to retransmit their commit_sig.
+ // TODO: replace by d.commitments.localCommitIndex + 1 when removing support for the legacy splice protocol.
val nextLocalCommitmentNumber = d match {
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match {
- case DualFundingStatus.RbfWaitingForSigs(status) => status.nextLocalCommitmentNumber
+ case DualFundingStatus.RbfWaitingForSigs(status) => status.nextLocalCommitmentNumber(d.channelParams.useLegacySpliceProtocol)
case _ => d.commitments.localCommitIndex + 1
}
case d: DATA_NORMAL => d.spliceStatus match {
- case SpliceStatus.SpliceWaitingForSigs(status) => status.nextLocalCommitmentNumber
+ case SpliceStatus.SpliceWaitingForSigs(status) => status.nextLocalCommitmentNumber(d.channelParams.useLegacySpliceProtocol)
case _ => d.commitments.localCommitIndex + 1
}
case _ => d.commitments.localCommitIndex + 1
}
- // If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures.
- val rbfTlv: Set[ChannelReestablishTlv] = d match {
- case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match {
- case DualFundingStatus.RbfWaitingForSigs(status) => Set(ChannelReestablishTlv.NextFundingTlv(status.fundingTx.txId))
- case _ => d.latestFundingTx.sharedTx match {
- case _: InteractiveTxBuilder.PartiallySignedSharedTransaction => Set(ChannelReestablishTlv.NextFundingTlv(d.latestFundingTx.sharedTx.txId))
- case _: InteractiveTxBuilder.FullySignedSharedTransaction => Set.empty
+ // If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures and commit_sig.
+ val rbfTlv: Set[ChannelReestablishTlv] = if (d.channelParams.useLegacySpliceProtocol) {
+ d match {
+ case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match {
+ case DualFundingStatus.RbfWaitingForSigs(status) => Set(ChannelReestablishTlv.ExperimentalNextFundingTlv(status.fundingTx.txId))
+ case _ => d.latestFundingTx.sharedTx match {
+ case _: InteractiveTxBuilder.PartiallySignedSharedTransaction => Set(ChannelReestablishTlv.ExperimentalNextFundingTlv(d.latestFundingTx.sharedTx.txId))
+ case _: InteractiveTxBuilder.FullySignedSharedTransaction => Set.empty
+ }
+ }
+ case d: DATA_NORMAL => d.spliceStatus match {
+ case SpliceStatus.SpliceWaitingForSigs(status) => Set(ChannelReestablishTlv.ExperimentalNextFundingTlv(status.fundingTx.txId))
+ case _ => d.commitments.latest.localFundingStatus match {
+ case LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _, _) => Set(ChannelReestablishTlv.ExperimentalNextFundingTlv(fundingTx.txId))
+ case _ => Set.empty
+ }
}
+ case _ => Set.empty
}
- case d: DATA_NORMAL => d.spliceStatus match {
- case SpliceStatus.SpliceWaitingForSigs(status) => Set(ChannelReestablishTlv.NextFundingTlv(status.fundingTx.txId))
- case _ => d.commitments.latest.localFundingStatus match {
- case LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _, _) => Set(ChannelReestablishTlv.NextFundingTlv(fundingTx.txId))
- case _ => Set.empty
+ } else {
+ d match {
+ case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match {
+ case DualFundingStatus.RbfWaitingForSigs(status) => Set(ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv.asNextFunding(status.fundingTx.txId, status.retransmitRemoteCommitSig))
+ case _ => d.latestFundingTx.sharedTx match {
+ case _: InteractiveTxBuilder.PartiallySignedSharedTransaction => Set(ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv.asNextFunding(d.latestFundingTx.sharedTx.txId, retransmitCommitSig = false))
+ case _: InteractiveTxBuilder.FullySignedSharedTransaction => Set.empty
+ }
}
+ case d: DATA_NORMAL => d.spliceStatus match {
+ case SpliceStatus.SpliceWaitingForSigs(status) => Set(ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv.asNextFunding(status.fundingTx.txId, status.retransmitRemoteCommitSig))
+ case _ => d.commitments.latest.localFundingStatus match {
+ case LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _, _) => Set(ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv.asNextFunding(fundingTx.txId, retransmitCommitSig = false))
+ case _ => Set.empty
+ }
+ }
+ case _ => Set.empty
}
- case _ => Set.empty
}
- val lastFundingLockedTlvs: Set[ChannelReestablishTlv] = if (d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype)) {
- d.commitments.lastLocalLocked_opt.map(c => ChannelReestablishTlv.MyCurrentFundingLockedTlv(c.fundingTxId)).toSet ++
- d.commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv.YourLastFundingLockedTlv(c.fundingTxId)).toSet
- } else Set.empty
+ val lastFundingLockedTlvs: Set[ChannelReestablishTlv] = if (d.channelParams.useLegacySpliceProtocol) {
+ val myCurrentFundingLocked_opt = d.commitments.lastLocalLocked_opt.map(c => ChannelReestablishTlv.ExperimentalMyCurrentFundingLockedTlv(c.fundingTxId))
+ val yourLastFundingLocked_opt = d.commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv.asExperimentalYourLastFundingLocked(c.fundingTxId))
+ myCurrentFundingLocked_opt.toSet ++ yourLastFundingLocked_opt.toSet
+ } else if (d.channelParams.remoteParams.initFeatures.hasFeature(Features.Splicing)) {
+ d.commitments.lastLocalLocked_opt.map(c => {
+ // We ask our peer to retransmit their announcement_signatures if we haven't already announced that splice.
+ val retransmitAnnSigs = d match {
+ case d: DATA_NORMAL if d.commitments.announceChannel => !d.lastAnnouncedFundingTxId_opt.contains(c.fundingTxId)
+ case _ => false
+ }
+ ChannelReestablishTlv.MyCurrentFundingLockedTlv(c.fundingTxId, retransmitAnnSigs)
+ }).toSet
+ } else {
+ Set.empty
+ }
// We send our verification nonces for all active commitments.
val nextCommitNonces: Map[TxId, IndividualNonce] = d.commitments.active.flatMap(c => {
@@ -2562,8 +2603,13 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
handleLocalError(f, d, Some(channelReestablish))
case _ =>
remoteNextCommitNonces = channelReestablish.nextCommitNonces
+ val retransmitCommitSig = if (d.channelParams.useLegacySpliceProtocol) {
+ channelReestablish.nextLocalCommitmentNumber == 0
+ } else {
+ channelReestablish.retransmitInteractiveTxCommitSig
+ }
channelReestablish.nextFundingTxId_opt match {
- case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && channelReestablish.nextLocalCommitmentNumber == 0 =>
+ case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && retransmitCommitSig =>
// They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received
// their commit_sig or their tx_signatures (depending on who must send tx_signatures first).
val fundingParams = d.signingSession.fundingParams
@@ -2587,11 +2633,16 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Some(f) => handleLocalError(f, d, Some(channelReestablish))
case None =>
remoteNextCommitNonces = channelReestablish.nextCommitNonces
+ val retransmitCommitSig = if (d.channelParams.useLegacySpliceProtocol) {
+ channelReestablish.nextLocalCommitmentNumber == 0
+ } else {
+ channelReestablish.retransmitInteractiveTxCommitSig
+ }
channelReestablish.nextFundingTxId_opt match {
case Some(fundingTxId) =>
d.status match {
case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId =>
- if (channelReestablish.nextLocalCommitmentNumber == 0) {
+ if (retransmitCommitSig) {
// They haven't received our commit_sig: we retransmit it.
// We're also waiting for signatures from them, and will send our tx_signatures once we receive them.
val fundingParams = signingSession.fundingParams
@@ -2608,7 +2659,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case _ if d.latestFundingTx.sharedTx.txId == fundingTxId =>
// We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
// and our commit_sig if they haven't received it already.
- if (channelReestablish.nextLocalCommitmentNumber == 0) {
+ if (retransmitCommitSig) {
val remoteNonce_opt = channelReestablish.currentCommitNonce_opt
d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match {
case Left(e) => handleLocalError(e, d, Some(channelReestablish))
@@ -2643,10 +2694,15 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
val channelReady = createChannelReady(d.aliases, d.commitments)
// We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
// and our commit_sig if they haven't received it already.
+ val retransmitCommitSig = if (d.channelParams.useLegacySpliceProtocol) {
+ channelReestablish.nextLocalCommitmentNumber == 0
+ } else {
+ channelReestablish.retransmitInteractiveTxCommitSig
+ }
channelReestablish.nextFundingTxId_opt match {
case Some(fundingTxId) if fundingTxId == d.commitments.latest.fundingTxId =>
d.commitments.latest.localFundingStatus.localSigs_opt match {
- case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 =>
+ case Some(txSigs) if retransmitCommitSig =>
log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
val remoteNonce_opt = channelReestablish.currentCommitNonce_opt
d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match {
@@ -2699,15 +2755,16 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case None =>
remoteNextCommitNonces = channelReestablish.nextCommitNonces
// We re-send our latest splice_locked if needed.
- val spliceLocked_opt = resendSpliceLockedIfNeeded(channelReestablish, commitments1, d.lastAnnouncement_opt)
- sendQueue = sendQueue ++ spliceLocked_opt.toSeq
+ val spliceLocked_opt = resendSpliceLockedIfNeeded(commitments1)
+ // We retransmit our latest announcement_signatures if our peer requests it.
+ val spliceAnnSigs_opt = resendSpliceAnnSigsIfNeeded(channelReestablish, commitments1)
+ sendQueue = sendQueue ++ spliceLocked_opt.toSeq ++ spliceAnnSigs_opt.toSeq
// We may need to retransmit updates and/or commit_sig and/or revocation to resume the channel.
sendQueue = sendQueue ++ syncSuccess.retransmit
commitments1.remoteNextCommitInfo match {
- case Left(_) =>
- // we expect them to (re-)send the revocation immediately
- startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout)
+ // we expect them to (re-)send their revocation immediately
+ case Left(_) => startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout)
case _ => ()
}
@@ -3398,22 +3455,20 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
// We only send channel_ready for initial funding transactions.
case Some(c) if c.fundingTxIndex != 0 => (None, None)
case Some(c) =>
- val remoteSpliceSupport = d.commitments.remoteChannelParams.initFeatures.hasFeature(Features.SplicePrototype)
- // If our peer has not received our channel_ready, we retransmit it.
- val notReceivedByRemote = remoteSpliceSupport && channelReestablish.yourLastFundingLocked_opt.isEmpty
// 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
- val notReceivedByRemoteLegacy = !remoteSpliceSupport && channelReestablish.nextLocalCommitmentNumber == 1 && c.localCommit.index == 0
+ val notReceivedByRemote = channelReestablish.nextLocalCommitmentNumber == 1 && c.localCommit.index == 0
// If this is a public channel and we haven't announced the channel, we retransmit our channel_ready and
// will also send announcement_signatures.
val notAnnouncedYet = d.commitments.announceChannel && c.shortChannelId_opt.nonEmpty && d.lastAnnouncement_opt.isEmpty
- val channelReady_opt = if (notAnnouncedYet || notReceivedByRemote || notReceivedByRemoteLegacy) {
+ // If our peer is a phoenix wallet using the legacy splicing protocol, we always retransmit channel_ready.
+ val channelReady_opt = if (notAnnouncedYet || notReceivedByRemote || d.channelParams.useLegacySpliceProtocol) {
log.debug("re-sending channel_ready")
Some(createChannelReady(d.aliases, d.commitments))
} else {
None
}
- val announcementSigs_opt = if (notAnnouncedYet) {
+ val announcementSigs_opt = if (notAnnouncedYet || (channelReestablish.retransmitAnnSigs && channelReestablish.myCurrentFundingLocked_opt.contains(c.fundingTxId))) {
// The funding transaction is confirmed, so we've already sent our announcement_signatures.
// We haven't announced the channel yet, which means we haven't received our peer's announcement_signatures.
// We retransmit our announcement_signatures to let our peer know that we're ready to announce the channel.
@@ -3429,11 +3484,16 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
private def resumeSpliceSigningSessionIfNeeded(channelReestablish: ChannelReestablish, d: DATA_NORMAL): (SpliceStatus, Queue[LightningMessage]) = {
var sendQueue = Queue.empty[LightningMessage]
+ val retransmitCommitSig = if (d.channelParams.useLegacySpliceProtocol) {
+ channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex
+ } else {
+ channelReestablish.retransmitInteractiveTxCommitSig
+ }
val spliceStatus1 = channelReestablish.nextFundingTxId_opt match {
case Some(fundingTxId) =>
d.spliceStatus match {
case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId =>
- if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) {
+ if (retransmitCommitSig) {
// They haven't received our commit_sig: we retransmit it.
// We're also waiting for signatures from them, and will send our tx_signatures once we receive them.
log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId)
@@ -3450,7 +3510,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case dfu: LocalFundingStatus.DualFundedUnconfirmedFundingTx =>
// We've already received their commit_sig and sent our tx_signatures. We retransmit our
// tx_signatures and our commit_sig if they haven't received it already.
- if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) {
+ if (retransmitCommitSig) {
log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
val remoteNonce_opt = channelReestablish.currentCommitNonce_opt
d.commitments.latest.remoteCommit.sign(d.commitments.channelParams, d.commitments.latest.remoteCommitParams, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys), d.commitments.latest.commitmentFormat, remoteNonce_opt) match {
@@ -3479,23 +3539,15 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
(spliceStatus1, sendQueue)
}
- private def resendSpliceLockedIfNeeded(channelReestablish: ChannelReestablish, commitments: Commitments, lastAnnouncement_opt: Option[ChannelAnnouncement]): Option[SpliceLocked] = {
+ private def resendSpliceLockedIfNeeded(commitments: Commitments): Option[SpliceLocked] = {
commitments.lastLocalLocked_opt match {
case None => None
// We only send splice_locked for splice transactions.
case Some(c) if c.fundingTxIndex == 0 => None
case Some(c) =>
- // If our peer has not received our splice_locked, we retransmit it.
- val notReceivedByRemote = !channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId)
- // If this is a public channel and we haven't announced the splice, we retransmit our splice_locked and
- // will exchange announcement_signatures afterwards.
- val notAnnouncedYet = commitments.announceChannel && lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId))
- if (notReceivedByRemote || notAnnouncedYet) {
- // Retransmission of local announcement_signatures for splices are done when receiving splice_locked, no need
- // to retransmit here.
+ // We only send splice_locked for legacy phoenix wallets using the old splicing protocol.
+ if (commitments.channelParams.useLegacySpliceProtocol) {
log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId)
- spliceLockedSent += (c.fundingTxId -> c.fundingTxIndex)
- trimSpliceLockedSentIfNeeded()
Some(SpliceLocked(commitments.channelId, c.fundingTxId))
} else {
None
@@ -3503,6 +3555,22 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
}
}
+ private def resendSpliceAnnSigsIfNeeded(channelReestablish: ChannelReestablish, commitments: Commitments): Option[AnnouncementSignatures] = {
+ commitments.lastLocalLocked_opt match {
+ case None => None
+ // This retransmit mechanism is only available for splice transactions.
+ case Some(c) if c.fundingTxIndex == 0 => None
+ case Some(c) if channelReestablish.retransmitAnnSigs && commitments.announceChannel && channelReestablish.myCurrentFundingLocked_opt.contains(c.fundingTxId) =>
+ val localAnnSigs = c.signAnnouncement(nodeParams, commitments.channelParams, channelKeys.fundingKey(c.fundingTxIndex))
+ localAnnSigs.foreach(annSigs => {
+ log.debug("re-sending announcement_signatures for fundingTxId={}", c.fundingTxId)
+ announcementSigsSent += annSigs.shortChannelId
+ })
+ localAnnSigs
+ case _ => None
+ }
+ }
+
/**
* Return full information about a known closing tx.
*/
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala
index 5edd456..b054ecc 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala
@@ -1206,10 +1206,14 @@ object InteractiveTxSigningSession {
liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession {
val fundingTxId: TxId = fundingTx.txId
val localCommitIndex: Long = localCommit.fold(_.index, _.index)
- // This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not.
- val nextLocalCommitmentNumber: Long = localCommit match {
- case Left(unsignedCommit) => unsignedCommit.index
- case Right(commit) => commit.index + 1
+ // If we haven't received the remote commit_sig, we will request a retransmission on reconnection.
+ val retransmitRemoteCommitSig: Boolean = localCommit.isLeft
+
+ // For the legacy splice protocol, we use the next_commitment_number to let our peer know whether they needed to
+ // retransmit commit_sig or not. We're now using an explicit bit instead, but need to maintain backwards-compatibility.
+ def nextLocalCommitmentNumber(useLegacySpliceProtocol: Boolean): Long = localCommit match {
+ case Left(unsignedCommit) if useLegacySpliceProtocol => unsignedCommit.index
+ case _ => localCommitIndex + 1
}
def localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala
index 12aa113..4a81d38 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala
@@ -207,12 +207,20 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
stay()
case Event(msg: LightningMessage, d: ConnectedData) if sender() != d.transport => // if the message doesn't originate from the transport, it is an outgoing message
+ val useExperimentalSplice = d.remoteInit.features.hasFeature(Features.SplicePrototype)
msg match {
+ // If our peer is using the experimental splice version, we convert splice messages.
+ case msg: SpliceInit if useExperimentalSplice => d.transport forward ExperimentalSpliceInit.from(msg)
+ case msg: SpliceAck if useExperimentalSplice => d.transport forward ExperimentalSpliceAck.from(msg)
+ case msg: SpliceLocked if useExperimentalSplice => d.transport forward ExperimentalSpliceLocked.from(msg)
+ case msg: TxAddInput if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[TxAddInputTlv.SharedInputTxId])))
+ case msg: TxSignatures if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[TxSignaturesTlv.PreviousFundingTxSig])))
+ case batch: CommitSigBatch if useExperimentalSplice => batch.messages.foreach(msg => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.FundingTx]))))
case batch: CommitSigBatch =>
// We insert a start_batch message to let our peer know how many commit_sig they will receive.
d.transport forward StartBatch.commitSigBatch(batch.channelId, batch.batchSize)
- batch.messages.foreach(msg => d.transport forward msg)
- case msg => d.transport forward msg
+ batch.messages.foreach(msg => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.ExperimentalBatchTlv]))))
+ case _ => d.transport forward msg
}
msg match {
// If we send any channel management message to this peer, the connection should be persistent.
@@ -426,6 +434,16 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
d.peer ! msg
stay()
}
+ // If our peer is using the experimental splice version, we convert splice messages.
+ case msg: ExperimentalSpliceInit =>
+ d.peer ! msg.toSpliceInit
+ stay()
+ case msg: ExperimentalSpliceAck =>
+ d.peer ! msg.toSpliceAck
+ stay()
+ case msg: ExperimentalSpliceLocked =>
+ d.peer ! msg.toSpliceLocked
+ stay()
case _ =>
d.peer ! msg
stay()
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala
index 9aad296..6c6ec2e 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala
@@ -17,7 +17,7 @@
package fr.acinq.eclair.wire.protocol
import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce
-import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, TxId}
+import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, TxHash, TxId}
import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
import fr.acinq.eclair.channel.{ChannelType, ChannelTypes}
import fr.acinq.eclair.wire.protocol.CommonCodecs._
@@ -255,17 +255,48 @@ sealed trait ChannelReestablishTlv extends Tlv
object ChannelReestablishTlv {
+ /** TODO: replaced by [[NextFundingTlv]], remove once Phoenix users have upgraded. */
+ case class ExperimentalNextFundingTlv(txId: TxId) extends ChannelReestablishTlv
+
+ /**
+ * We unfortunately have a conflict between the splicing protocol we use for Phoenix and the official one.
+ * The official protocol uses TLV = 1 for the next_funding TLV, which should be implemented by [[NextFundingTlv]]
+ * (which is commented out below).
+ * The splicing protocol used for Phoenix also uses TLV = 1, but for a different TLV that was removed from the
+ * official splicing protocol (your_last_funding_locked), which contained the txid of the last [[ChannelReady]] or
+ * [[SpliceLocked]] message received before disconnecting, if any.
+ *
+ * To guarantee backwards-compatibility, we create a TLV field that may contain both options. When using the official
+ * splicing protocol, it will contain 33 bytes (a txid and a bitfield), while when using the legacy protocol it only
+ * contains a txid, which lets us easily distinguish the two.
+ *
+ * TODO: once we can remove support for Phoenix users with the legacy splicing protocol, this should just be replaced
+ * by [[NextFundingTlv]] which is commented out below and should be uncommented.
+ */
+ case class NextFundingOrExperimentalYourLastFundingLockedTlv(data: ByteVector) extends ChannelReestablishTlv {
+ val isOfficial: Boolean = data.size == 33
+ val txId: TxId = TxId(TxHash(ByteVector32(data.take(32))))
+ // NB: this is only used for the official splicing protocol.
+ val retransmitCommitSig: Boolean = if (isOfficial) (data.last.toInt % 2) == 1 else false
+ }
+
/**
* When disconnected in the middle of an interactive-tx session, this field is used to request a retransmission of
* [[TxSignatures]] for the given [[txId]].
+ *
+ * @param txId the txid of the partially signed funding transaction.
+ * @param retransmitCommitSig true if [[CommitSig]] must be retransmitted before [[TxSignatures]].
*/
- case class NextFundingTlv(txId: TxId) extends ChannelReestablishTlv
+ // case class NextFundingTlv(txId: TxId, retransmitCommitSig: Boolean) extends ChannelReestablishTlv
- /** The txid of the last [[ChannelReady]] or [[SpliceLocked]] message received before disconnecting, if any. */
- case class YourLastFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
+ /** TODO: replaced by [[MyCurrentFundingLockedTlv]], remove once Phoenix users have upgraded. */
+ case class ExperimentalMyCurrentFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
- /** The txid of our latest outgoing [[ChannelReady]] or [[SpliceLocked]] for this channel. */
- case class MyCurrentFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
+ /**
+ * @param txId the txid of our latest outgoing [[ChannelReady]] or [[SpliceLocked]] for this channel.
+ * @param retransmitAnnSigs true if [[AnnouncementSignatures]] must be retransmitted.
+ */
+ case class MyCurrentFundingLockedTlv(txId: TxId, retransmitAnnSigs: Boolean) extends ChannelReestablishTlv
/**
* When disconnected during an interactive tx session, we'll include a verification nonce for our *current* commitment
@@ -280,16 +311,31 @@ object ChannelReestablishTlv {
*/
case class NextLocalNoncesTlv(nonces: Seq[(TxId, IndividualNonce)]) extends ChannelReestablishTlv
- object NextFundingTlv {
- val codec: Codec[NextFundingTlv] = tlvField(txIdAsHash)
+ object ExperimentalNextFundingTlv {
+ val codec: Codec[ExperimentalNextFundingTlv] = tlvField(txIdAsHash)
+ }
+
+ // object NextFundingTlv {
+ // val codec: Codec[NextFundingTlv] = tlvField(("next_funding_txid" | txIdAsHash) :: ("retransmit_flags" | (ignore(7) :: bool)))
+ // }
+
+ object NextFundingOrExperimentalYourLastFundingLockedTlv {
+ def asNextFunding(txId: TxId, retransmitCommitSig: Boolean): NextFundingOrExperimentalYourLastFundingLockedTlv = {
+ val retransmitFlags = if (retransmitCommitSig) ByteVector.fromValidHex("01") else ByteVector.fromValidHex("00")
+ NextFundingOrExperimentalYourLastFundingLockedTlv(TxHash(txId).value ++ retransmitFlags)
+ }
+
+ def asExperimentalYourLastFundingLocked(txId: TxId): NextFundingOrExperimentalYourLastFundingLockedTlv = NextFundingOrExperimentalYourLastFundingLockedTlv(TxHash(txId).value)
+
+ val codec: Codec[NextFundingOrExperimentalYourLastFundingLockedTlv] = tlvField(bytes)
}
- object YourLastFundingLockedTlv {
- val codec: Codec[YourLastFundingLockedTlv] = tlvField("your_last_funding_locked_txid" | txIdAsHash)
+ object ExperimentalMyCurrentFundingLockedTlv {
+ val codec: Codec[ExperimentalMyCurrentFundingLockedTlv] = tlvField("my_current_funding_locked_txid" | txIdAsHash)
}
object MyCurrentFundingLockedTlv {
- val codec: Codec[MyCurrentFundingLockedTlv] = tlvField("my_current_funding_locked_txid" | txIdAsHash)
+ val codec: Codec[MyCurrentFundingLockedTlv] = tlvField(("my_current_funding_locked_txid" | txIdAsHash) :: ("retransmit_flags" | (ignore(7) :: bool)))
}
object CurrentCommitNonceTlv {
@@ -301,9 +347,12 @@ object ChannelReestablishTlv {
}
val channelReestablishTlvCodec: Codec[TlvStream[ChannelReestablishTlv]] = tlvStream(discriminated[ChannelReestablishTlv].by(varint)
- .typecase(UInt64(0), NextFundingTlv.codec)
- .typecase(UInt64(1), YourLastFundingLockedTlv.codec)
- .typecase(UInt64(3), MyCurrentFundingLockedTlv.codec)
+ .typecase(UInt64(0), ExperimentalNextFundingTlv.codec)
+ // TODO: replace with the commented line below when removing support for the legacy splicing protocol.
+ .typecase(UInt64(1), NextFundingOrExperimentalYourLastFundingLockedTlv.codec)
+ // .typecase(UInt64(1), NextFundingTlv.codec)
+ .typecase(UInt64(3), ExperimentalMyCurrentFundingLockedTlv.codec)
+ .typecase(UInt64(5), MyCurrentFundingLockedTlv.codec)
.typecase(UInt64(22), NextLocalNoncesTlv.codec)
.typecase(UInt64(24), CurrentCommitNonceTlv.codec)
)
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala
index f8ae3d1..3eb0c34 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala
@@ -36,6 +36,9 @@ object TxAddInputTlv {
/** When doing a splice, the initiator must provide the previous funding txId instead of the whole transaction. */
case class SharedInputTxId(txId: TxId) extends TxAddInputTlv
+ /** Same as [[SharedInputTxId]] for peers who only support the experimental version of splicing. */
+ case class ExperimentalSharedInputTxId(txId: TxId) extends TxAddInputTlv
+
/**
* When creating an interactive-tx where both participants sign a taproot input, we don't need to provide the entire
* previous transaction in [[TxAddInput]]: signatures will commit to the txOut of *all* of the transaction's inputs,
@@ -49,7 +52,8 @@ object TxAddInputTlv {
val txAddInputTlvCodec: Codec[TlvStream[TxAddInputTlv]] = tlvStream(discriminated[TxAddInputTlv].by(varint)
// Note that we actually encode as a tx_hash to be consistent with other lightning messages.
- .typecase(UInt64(1105), tlvField(txIdAsHash.as[SharedInputTxId]))
+ .typecase(UInt64(0), tlvField(txIdAsHash.as[SharedInputTxId]))
+ .typecase(UInt64(1105), tlvField(txIdAsHash.as[ExperimentalSharedInputTxId]))
.typecase(UInt64(1111), PrevTxOut.codec)
)
}
@@ -102,9 +106,13 @@ object TxSignaturesTlv {
/** When doing a splice for a taproot channel, each peer must provide their partial signature for the previous musig2 funding output. */
case class PreviousFundingTxPartialSig(partialSigWithNonce: PartialSignatureWithNonce) extends TxSignaturesTlv
+ /** Same as [[PreviousFundingTxSig]] for peers who only support the experimental version of splicing. */
+ case class ExperimentalPreviousFundingTxSig(sig: ByteVector64) extends TxSignaturesTlv
+
val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint)
+ .typecase(UInt64(0), tlvField(bytes64.as[PreviousFundingTxSig]))
.typecase(UInt64(2), tlvField(partialSignatureWithNonce.as[PreviousFundingTxPartialSig]))
- .typecase(UInt64(601), tlvField(bytes64.as[PreviousFundingTxSig]))
+ .typecase(UInt64(601), tlvField(bytes64.as[ExperimentalPreviousFundingTxSig]))
)
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala
index f36c6d7..95797a7 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala
@@ -437,17 +437,36 @@ object LightningMessageCodecs {
("fundingPubkey" | publicKey) ::
("tlvStream" | SpliceInitTlv.spliceInitTlvCodec)).as[SpliceInit]
+ val experimentalSpliceInitCodec: Codec[ExperimentalSpliceInit] = (
+ ("channelId" | bytes32) ::
+ ("fundingContribution" | satoshiSigned) ::
+ ("feerate" | feeratePerKw) ::
+ ("lockTime" | uint32) ::
+ ("fundingPubkey" | publicKey) ::
+ ("tlvStream" | SpliceInitTlv.spliceInitTlvCodec)).as[ExperimentalSpliceInit]
+
val spliceAckCodec: Codec[SpliceAck] = (
("channelId" | bytes32) ::
("fundingContribution" | satoshiSigned) ::
("fundingPubkey" | publicKey) ::
("tlvStream" | SpliceAckTlv.spliceAckTlvCodec)).as[SpliceAck]
+ val experimentalSpliceAckCodec: Codec[ExperimentalSpliceAck] = (
+ ("channelId" | bytes32) ::
+ ("fundingContribution" | satoshiSigned) ::
+ ("fundingPubkey" | publicKey) ::
+ ("tlvStream" | SpliceAckTlv.spliceAckTlvCodec)).as[ExperimentalSpliceAck]
+
val spliceLockedCodec: Codec[SpliceLocked] = (
("channelId" | bytes32) ::
("fundingTxHash" | txIdAsHash) ::
("tlvStream" | SpliceLockedTlv.spliceLockedTlvCodec)).as[SpliceLocked]
+ val experimentalSpliceLockedCodec: Codec[ExperimentalSpliceLocked] = (
+ ("channelId" | bytes32) ::
+ ("fundingTxHash" | txIdAsHash) ::
+ ("tlvStream" | SpliceLockedTlv.spliceLockedTlvCodec)).as[ExperimentalSpliceLocked]
+
val stfuCodec: Codec[Stfu] = (
("channelId" | bytes32) ::
("initiator" | byte.xmap[Boolean](b => b != 0, b => if (b) 1 else 0))).as[Stfu]
@@ -530,6 +549,9 @@ object LightningMessageCodecs {
.typecase(72, txInitRbfCodec)
.typecase(73, txAckRbfCodec)
.typecase(74, txAbortCodec)
+ .typecase(77, spliceLockedCodec)
+ .typecase(80, spliceInitCodec)
+ .typecase(81, spliceAckCodec)
.typecase(127, startBatchCodec)
.typecase(128, updateAddHtlcCodec)
.typecase(130, updateFulfillHtlcCodec)
@@ -562,9 +584,9 @@ object LightningMessageCodecs {
.typecase(41045, addFeeCreditCodec)
.typecase(41046, currentFeeCreditCodec)
//
- .typecase(37000, spliceInitCodec)
- .typecase(37002, spliceAckCodec)
- .typecase(37004, spliceLockedCodec)
+ .typecase(37000, experimentalSpliceInitCodec)
+ .typecase(37002, experimentalSpliceAckCodec)
+ .typecase(37004, experimentalSpliceLockedCodec)
//
//
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala
index 0ceb575..8f405b5 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala
@@ -97,13 +97,14 @@ case class TxAddInput(channelId: ByteVector32,
tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId {
/** This field may replace [[previousTx_opt]] when using taproot. */
val previousTxOut_opt: Option[InputInfo] = tlvStream.get[TxAddInputTlv.PrevTxOut].map(tlv => InputInfo(OutPoint(tlv.txId, previousTxOutput), TxOut(tlv.amount, tlv.publicKeyScript)))
- val sharedInput_opt: Option[OutPoint] = tlvStream.get[TxAddInputTlv.SharedInputTxId].map(i => OutPoint(i.txId, previousTxOutput))
+ val sharedInput_opt: Option[OutPoint] = tlvStream.get[TxAddInputTlv.SharedInputTxId].map(i => OutPoint(i.txId, previousTxOutput)).orElse(tlvStream.get[TxAddInputTlv.ExperimentalSharedInputTxId].map(i => OutPoint(i.txId, previousTxOutput)))
}
object TxAddInput {
def apply(channelId: ByteVector32, serialId: UInt64, sharedInput: OutPoint, sequence: Long): TxAddInput = {
val tlvs = Set[TxAddInputTlv](
TxAddInputTlv.SharedInputTxId(sharedInput.txid),
+ TxAddInputTlv.ExperimentalSharedInputTxId(sharedInput.txid),
)
TxAddInput(channelId, serialId, None, sharedInput.index, sequence, TlvStream(tlvs))
}
@@ -143,17 +144,27 @@ case class TxSignatures(channelId: ByteVector32,
txId: TxId,
witnesses: Seq[ScriptWitness],
tlvStream: TlvStream[TxSignaturesTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId {
- val previousFundingTxSig_opt: Option[ByteVector64] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].map(_.sig)
+ val previousFundingTxSig_opt: Option[ByteVector64] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].map(_.sig).orElse(tlvStream.get[TxSignaturesTlv.ExperimentalPreviousFundingTxSig].map(_.sig))
val previousFundingTxPartialSig_opt: Option[PartialSignatureWithNonce] = tlvStream.get[TxSignaturesTlv.PreviousFundingTxPartialSig].map(_.partialSigWithNonce)
}
object TxSignatures {
- def apply(channelId: ByteVector32, tx: Transaction, witnesses: Seq[ScriptWitness], previousFundingSig_opt: Option[ChannelSpendSignature]): TxSignatures = {
- val tlvs: Set[TxSignaturesTlv] = previousFundingSig_opt match {
- case Some(IndividualSignature(sig)) => Set(TxSignaturesTlv.PreviousFundingTxSig(sig))
- case Some(partialSig: PartialSignatureWithNonce) => Set(TxSignaturesTlv.PreviousFundingTxPartialSig(partialSig))
- case None => Set.empty
- }
+ def apply(channelId: ByteVector32,
+ tx: Transaction,
+ witnesses: Seq[ScriptWitness],
+ previousFundingSig_opt: Option[ChannelSpendSignature]): TxSignatures = {
+ val tlvs: Set[TxSignaturesTlv] = Set(
+ previousFundingSig_opt match {
+ case Some(IndividualSignature(sig)) => Some(TxSignaturesTlv.PreviousFundingTxSig(sig))
+ case Some(partialSig: PartialSignatureWithNonce) => Some(TxSignaturesTlv.PreviousFundingTxPartialSig(partialSig))
+ case None => None
+ },
+ // We keep supporting the experimental splicing protocol.
+ previousFundingSig_opt match {
+ case Some(IndividualSignature(sig)) => Some(TxSignaturesTlv.ExperimentalPreviousFundingTxSig(sig))
+ case _ => None
+ }
+ ).flatten
TxSignatures(channelId, tx.txid, witnesses, TlvStream(tlvs))
}
}
@@ -211,9 +222,17 @@ case class ChannelReestablish(channelId: ByteVector32,
yourLastPerCommitmentSecret: PrivateKey,
myCurrentPerCommitmentPoint: PublicKey,
tlvStream: TlvStream[ChannelReestablishTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
- val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].map(_.txId)
- val myCurrentFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].map(_.txId)
- val yourLastFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.YourLastFundingLockedTlv].map(_.txId)
+ val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv] match {
+ case Some(tlv) if tlv.isOfficial => Some(tlv.txId)
+ case _ => tlvStream.get[ChannelReestablishTlv.ExperimentalNextFundingTlv].map(_.txId)
+ }
+ val retransmitInteractiveTxCommitSig: Boolean = tlvStream.get[ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv].exists(_.retransmitCommitSig)
+ val myCurrentFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].map(_.txId).orElse(tlvStream.get[ChannelReestablishTlv.ExperimentalMyCurrentFundingLockedTlv].map(_.txId))
+ val yourLastFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv] match {
+ case Some(tlv) if !tlv.isOfficial => Some(tlv.txId)
+ case _ => None
+ }
+ val retransmitAnnSigs: Boolean = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].exists(_.retransmitAnnSigs)
val nextCommitNonces: Map[TxId, IndividualNonce] = tlvStream.get[ChannelReestablishTlv.NextLocalNoncesTlv].map(_.nonces.toMap).getOrElse(Map.empty)
val currentCommitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelReestablishTlv.CurrentCommitNonceTlv].map(_.nonce)
}
@@ -411,6 +430,19 @@ object SpliceInit {
apply(channelId, fundingContribution, lockTime, feerate, fundingPubKey, pushAmount, requireConfirmedInputs, requestFunding_opt, None)
}
+case class ExperimentalSpliceInit(channelId: ByteVector32,
+ fundingContribution: Satoshi,
+ feerate: FeeratePerKw,
+ lockTime: Long,
+ fundingPubKey: PublicKey,
+ tlvStream: TlvStream[SpliceInitTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
+ def toSpliceInit: SpliceInit = SpliceInit(channelId, fundingContribution, feerate, lockTime, fundingPubKey, tlvStream)
+}
+
+object ExperimentalSpliceInit {
+ def from(msg: SpliceInit): ExperimentalSpliceInit = ExperimentalSpliceInit(msg.channelId, msg.fundingContribution, msg.feerate, msg.lockTime, msg.fundingPubKey, msg.tlvStream)
+}
+
case class SpliceAck(channelId: ByteVector32,
fundingContribution: Satoshi,
fundingPubKey: PublicKey,
@@ -437,11 +469,32 @@ object SpliceAck {
apply(channelId, fundingContribution, fundingPubKey, pushAmount, requireConfirmedInputs, willFund_opt, feeCreditUsed_opt, None)
}
+case class ExperimentalSpliceAck(channelId: ByteVector32,
+ fundingContribution: Satoshi,
+ fundingPubKey: PublicKey,
+ tlvStream: TlvStream[SpliceAckTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
+ def toSpliceAck: SpliceAck = SpliceAck(channelId, fundingContribution, fundingPubKey, tlvStream)
+}
+
+object ExperimentalSpliceAck {
+ def from(msg: SpliceAck): ExperimentalSpliceAck = ExperimentalSpliceAck(msg.channelId, msg.fundingContribution, msg.fundingPubKey, msg.tlvStream)
+}
+
case class SpliceLocked(channelId: ByteVector32,
fundingTxId: TxId,
tlvStream: TlvStream[SpliceLockedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
}
+case class ExperimentalSpliceLocked(channelId: ByteVector32,
+ fundingTxId: TxId,
+ tlvStream: TlvStream[SpliceLockedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
+ def toSpliceLocked: SpliceLocked = SpliceLocked(channelId, fundingTxId, tlvStream)
+}
+
+object ExperimentalSpliceLocked {
+ def from(msg: SpliceLocked): ExperimentalSpliceLocked = ExperimentalSpliceLocked(msg.channelId, msg.fundingTxId, msg.tlvStream)
+}
+
case class Shutdown(channelId: ByteVector32,
scriptPubKey: ByteVector,
tlvStream: TlvStream[ShutdownTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId with ForbiddenMessageWhenQuiescent {
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala
index 051ba6e..571e2b8 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala
@@ -117,7 +117,7 @@ object TestConstants {
Features.StaticRemoteKey -> FeatureSupport.Mandatory,
Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional,
Features.Quiescence -> FeatureSupport.Optional,
- Features.SplicePrototype -> FeatureSupport.Optional,
+ Features.Splicing -> FeatureSupport.Optional,
Features.ProvideStorage -> FeatureSupport.Optional,
Features.ChannelType -> FeatureSupport.Mandatory,
PluginFeature -> FeatureSupport.Optional
@@ -342,7 +342,7 @@ object TestConstants {
Features.StaticRemoteKey -> FeatureSupport.Mandatory,
Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional,
Features.Quiescence -> FeatureSupport.Optional,
- Features.SplicePrototype -> FeatureSupport.Optional,
+ Features.Splicing -> FeatureSupport.Optional,
Features.ChannelType -> FeatureSupport.Mandatory
),
pluginParams = Nil,
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala
index 1050912..1db149c 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala
@@ -9,7 +9,7 @@ import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.states.ChannelStateTestsBase
import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory
import fr.acinq.eclair.router.Announcements
-import fr.acinq.eclair.wire.protocol.{ChannelReestablish, ChannelUpdate, Init}
+import fr.acinq.eclair.wire.protocol.{ChannelReady, ChannelReestablish, ChannelUpdate, Init}
import fr.acinq.eclair.{TestKitBaseClass, _}
import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
@@ -130,6 +130,9 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan
assert(cup.channelUpdate.feeProportionalMillionths == newConfig.relayParams.privateChannelFees.feeProportionalMillionths)
assert(cup.channelUpdate.cltvExpiryDelta == newConfig.channelConf.expiryDelta)
+ alice2bob.ignoreMsg { case _: ChannelReady => true }
+ bob2alice.ignoreMsg { case _: ChannelReady => true }
+
newAlice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)
bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)
alice2bob.expectMsgType[ChannelReestablish]
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala
index 331e089..50e79fd 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala
@@ -273,7 +273,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.SimpleClose))(_.updated(Features.SimpleClose, FeatureSupport.Optional))
- .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableSplice))(_.removed(Features.SplicePrototype))
+ .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableSplice))(_.removed(Features.Splicing))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsPhoenix))(_.removed(Features.AnchorOutputsZeroFeeHtlcTx).updated(Features.AnchorOutputs, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaprootPhoenix))(_.removed(Features.SimpleTaprootChannelsStaging).updated(Features.SimpleTaprootChannelsPhoenix, FeatureSupport.Optional).updated(Features.PhoenixZeroReserve, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionSimpleTaproot))(_.updated(Features.SimpleTaprootChannelsStaging, FeatureSupport.Optional))
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala
index 7c2efb1..7f546e0 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala
@@ -486,13 +486,15 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit)
val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish]
- assert(channelReestablishAlice.nextLocalCommitmentNumber == 0)
+ assert(channelReestablishAlice.nextLocalCommitmentNumber == 1)
+ assert(channelReestablishAlice.retransmitInteractiveTxCommitSig)
assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty)
assert(channelReestablishAlice.nextCommitNonces.contains(fundingTxId))
bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit)
val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish]
- assert(channelReestablishBob.nextLocalCommitmentNumber == 0)
+ assert(channelReestablishBob.nextLocalCommitmentNumber == 1)
+ assert(channelReestablishBob.retransmitInteractiveTxCommitSig)
assert(channelReestablishBob.currentCommitNonce_opt.nonEmpty)
assert(channelReestablishBob.nextCommitNonces.contains(fundingTxId))
@@ -591,12 +593,14 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
assert(channelReestablishAlice.nextCommitNonces.contains(fundingTx.txid))
assert(channelReestablishAlice.nextCommitNonces.get(fundingTx.txid) != channelReestablishAlice.currentCommitNonce_opt)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(fundingTx.txid))
- assert(channelReestablishAlice.nextLocalCommitmentNumber == 0)
+ assert(channelReestablishAlice.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishAlice.nextLocalCommitmentNumber == 1)
alice2bob.forward(bob, channelReestablishAlice)
val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish]
assert(channelReestablishBob.currentCommitNonce_opt.isEmpty)
assert(channelReestablishBob.nextCommitNonces.get(fundingTx.txid) == channelReadyB.nextCommitNonce_opt)
assert(channelReestablishBob.nextFundingTxId_opt.isEmpty)
+ assert(!channelReestablishBob.retransmitInteractiveTxCommitSig)
assert(channelReestablishBob.nextLocalCommitmentNumber == 1)
bob2alice.forward(alice, channelReestablishBob)
@@ -734,14 +738,14 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
alice ! INPUT_RECONNECTED(bob, aliceInit, bobInit)
bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit)
val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish]
- val nextLocalCommitmentNumberAlice = if (aliceExpectsCommitSig) 0 else 1
assert(channelReestablishAlice.nextFundingTxId_opt.contains(fundingTxId))
- assert(channelReestablishAlice.nextLocalCommitmentNumber == nextLocalCommitmentNumberAlice)
+ assert(channelReestablishAlice.retransmitInteractiveTxCommitSig == aliceExpectsCommitSig)
+ assert(channelReestablishAlice.nextLocalCommitmentNumber == 1)
alice2bob.forward(bob, channelReestablishAlice)
val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish]
- val nextLocalCommitmentNumberBob = if (bobExpectsCommitSig) 0 else 1
assert(channelReestablishBob.nextFundingTxId_opt.contains(fundingTxId))
- assert(channelReestablishBob.nextLocalCommitmentNumber == nextLocalCommitmentNumberBob)
+ assert(channelReestablishBob.retransmitInteractiveTxCommitSig == bobExpectsCommitSig)
+ assert(channelReestablishBob.nextLocalCommitmentNumber == 1)
bob2alice.forward(alice, channelReestablishBob)
// When using taproot, we must provide nonces for the partial signatures.
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala
index 7566087..41ebb3b 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala
@@ -960,8 +960,10 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId))
- assert(channelReestablishAlice.nextLocalCommitmentNumber == 0)
+ assert(channelReestablishAlice.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishAlice.nextLocalCommitmentNumber == 1)
assert(channelReestablishBob.nextFundingTxId_opt.isEmpty)
+ assert(!channelReestablishBob.retransmitInteractiveTxCommitSig)
assert(channelReestablishBob.nextLocalCommitmentNumber == 1)
commitmentFormat match {
case _: SegwitV0CommitmentFormat => ()
@@ -1008,9 +1010,11 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId))
+ assert(!channelReestablishAlice.retransmitInteractiveTxCommitSig)
assert(channelReestablishAlice.nextLocalCommitmentNumber == 1)
assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId))
- assert(channelReestablishBob.nextLocalCommitmentNumber == 0)
+ assert(channelReestablishBob.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishBob.nextLocalCommitmentNumber == 1)
commitmentFormat match {
case _: SegwitV0CommitmentFormat => ()
case _: TaprootCommitmentFormat =>
@@ -1069,8 +1073,10 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
val (channelReestablishAlice, channelReestablishBob) = reconnectRbf(f)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId))
- assert(channelReestablishAlice.nextLocalCommitmentNumber == 0)
+ assert(channelReestablishAlice.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishAlice.nextLocalCommitmentNumber == 1)
assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId))
+ assert(!channelReestablishBob.retransmitInteractiveTxCommitSig)
assert(channelReestablishBob.nextLocalCommitmentNumber == 1)
commitmentFormat match {
case _: SegwitV0CommitmentFormat => ()
@@ -1137,7 +1143,8 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
bob ! INPUT_RECONNECTED(alice, bobInit, aliceInit)
val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish]
assert(channelReestablishAlice.nextFundingTxId_opt.nonEmpty)
- assert(channelReestablishAlice.nextLocalCommitmentNumber == 0)
+ assert(channelReestablishAlice.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishAlice.nextLocalCommitmentNumber == 1)
assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty)
bob2alice.expectMsgType[ChannelReestablish]
alice2bob.forward(bob, channelReestablishAlice.copy(tlvStream = TlvStream(channelReestablishAlice.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.CurrentCommitNonceTlv]))))
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala
index c122101..54c3a67 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala
@@ -238,18 +238,16 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF
val bobInit = Init(TestConstants.Bob.nodeParams.features.initFeatures())
alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)
bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)
- alice2bob.expectMsgType[ChannelReestablish]
+ assert(alice2bob.expectMsgType[ChannelReestablish].retransmitAnnSigs)
alice2bob.forward(bob)
- bob2alice.expectMsgType[ChannelReestablish]
+ assert(!bob2alice.expectMsgType[ChannelReestablish].retransmitAnnSigs)
bob2alice.forward(alice)
- // Bob does not retransmit channel_ready and announcement_signatures because he has already received both of them from Alice.
- bob2alice.expectNoMessage(100 millis)
- // Alice has already received Bob's channel_ready, but not its announcement_signatures.
- // She retransmits channel_ready and Bob will retransmit its announcement_signatures in response.
alice2bob.expectMsgType[ChannelReady]
alice2bob.forward(bob)
alice2bob.expectMsgType[AnnouncementSignatures]
alice2bob.forward(bob)
+ bob2alice.expectMsgType[ChannelReady]
+ bob2alice.forward(alice)
bob2alice.expectMsgType[AnnouncementSignatures]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.nonEmpty)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala
index dfab167..053d392 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala
@@ -275,6 +275,14 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.forall(c => c.fundingTxIndex > fundingTxIndex || c.fundingTxId == spliceTx.txid), interval = 100 millis)
}
+ private def getFundingScid(f: FixtureParam, fundingTx: Transaction): Option[RealShortChannelId] = {
+ import f._
+
+ val aliceScid_opt = alice.commitments.all.find(_.fundingTxId == fundingTx.txid).flatMap(_.shortChannelId_opt)
+ val bobScid_opt = bob.commitments.all.find(_.fundingTxId == fundingTx.txid).flatMap(_.shortChannelId_opt)
+ aliceScid_opt.orElse(bobScid_opt)
+ }
+
case class TestHtlcs(aliceToBob: Seq[(ByteVector32, UpdateAddHtlc)], bobToAlice: Seq[(ByteVector32, UpdateAddHtlc)])
private def setupHtlcs(f: FixtureParam): TestHtlcs = {
@@ -1507,9 +1515,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
bob2blockchain.expectMsg(UnwatchFundingSpent(fundingInput.txid, fundingInput.index.toInt))
assert(bob2alice.expectMsgType[SpliceLocked].fundingTxId == spliceTx1.txid)
bob2alice.forward(alice)
- alice2bob.expectMsgType[AnnouncementSignatures]
+ inside(alice2bob.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, spliceTx1).contains(ann.shortChannelId)) }
alice2bob.forward(bob)
val bobAnnSigs1 = bob2alice.expectMsgType[AnnouncementSignatures] // Alice doesn't receive Bob's signatures.
+ assert(getFundingScid(f, spliceTx1).contains(bobAnnSigs1.shortChannelId))
awaitAssert(assert(bob.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.exists(_ != ann)))
val spliceAnn1 = bob.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.get
assert(spliceAnn1.shortChannelId != ann.shortChannelId)
@@ -1540,9 +1549,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
bob2blockchain.expectMsg(UnwatchFundingSpent(fundingInput1.txid, fundingInput1.index.toInt))
assert(bob2alice.expectMsgType[SpliceLocked].fundingTxId == spliceTx2.txid)
bob2alice.forward(alice)
- alice2bob.expectMsgType[AnnouncementSignatures]
+ inside(alice2bob.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, spliceTx2).contains(ann.shortChannelId)) }
alice2bob.forward(bob)
val bobAnnSigs2 = bob2alice.expectMsgType[AnnouncementSignatures] // Alice doesn't receive Bob's signatures.
+ assert(getFundingScid(f, spliceTx2).contains(bobAnnSigs2.shortChannelId))
awaitAssert(assert(bob.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.exists(_ != spliceAnn1)))
val spliceAnn2 = bob.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.get
assert(spliceAnn2.shortChannelId != spliceAnn1.shortChannelId)
@@ -1580,7 +1590,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// Alice and Bob want to announce the initial funding transaction, but the messages are dropped.
val shortChannelId = alice2bob.expectMsgType[AnnouncementSignatures].shortChannelId
alice2bob.expectMsgType[ChannelUpdate]
- bob2alice.expectMsgType[AnnouncementSignatures]
+ inside(bob2alice.expectMsgType[AnnouncementSignatures]) { ann => assert(ann.shortChannelId == shortChannelId) }
bob2alice.expectMsgType[ChannelUpdate]
assert(alice.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.isEmpty)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.isEmpty)
@@ -1602,9 +1612,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
bob2blockchain.expectMsg(UnwatchFundingSpent(fundingInput.txid, fundingInput.index.toInt))
assert(bob2alice.expectMsgType[SpliceLocked].fundingTxId == spliceTx.txid)
bob2alice.forward(alice)
- alice2bob.expectMsgType[AnnouncementSignatures]
+ inside(alice2bob.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, spliceTx).contains(ann.shortChannelId)) }
alice2bob.forward(bob)
- bob2alice.expectMsgType[AnnouncementSignatures] // Alice doesn't receive Bob's signatures.
+ // Alice doesn't receive Bob's signatures.
+ inside(bob2alice.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, spliceTx).contains(ann.shortChannelId)) }
awaitAssert(bob.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.nonEmpty)
val spliceAnn = bob.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.get
assert(spliceAnn.shortChannelId != shortChannelId)
@@ -1623,16 +1634,15 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
awaitAssert(assert(alice.stateName == OFFLINE))
// Alice and Bob reconnect.
- reconnect(f)
- bob2alice.expectNoMessage(100 millis)
- assert(alice2bob.expectMsgType[SpliceLocked].fundingTxId == spliceTx.txid) // Alice resends `splice_locked` because she hasn't received Bob's announcement_signatures.
- alice2bob.forward(bob)
- alice2bob.expectNoMessage(100 millis)
- assert(bob2alice.expectMsgType[SpliceLocked].fundingTxId == spliceTx.txid) // Bob resends `splice_locked` in response to Alice's `splice_locked` after channel_reestablish.
- bob2alice.forward(alice)
+ val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
+ assert(channelReestablishAlice.retransmitAnnSigs)
+ assert(channelReestablishAlice.myCurrentFundingLocked_opt.contains(spliceTx.txid))
+ assert(!channelReestablishBob.retransmitAnnSigs)
+ assert(channelReestablishBob.myCurrentFundingLocked_opt.contains(spliceTx.txid))
assert(bob2alice.expectMsgType[AnnouncementSignatures].shortChannelId == spliceAnn.shortChannelId)
bob2alice.forward(alice)
bob2alice.expectNoMessage(100 millis)
+ alice2bob.expectNoMessage(100 millis)
assert(aliceListener.expectMsgType[ShortChannelIdAssigned].announcement_opt.contains(spliceAnn))
awaitAssert(assert(alice.stateData.asInstanceOf[DATA_NORMAL].lastAnnouncement_opt.contains(spliceAnn)))
awaitAssert(assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.all.size == 1))
@@ -1705,6 +1715,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// Bob disconnects before receiving Alice's commit_sig.
disconnect(f)
reconnect(f)
+ alice2bob.expectMsgType[ChannelReady]
+ alice2bob.forward(bob)
+ bob2alice.expectMsgType[ChannelReady]
+ bob2alice.forward(alice)
alice2bob.expectMsgType[UpdateAddHtlc]
alice2bob.forward(bob)
val sigsA = alice2bob.expectMsgType[CommitSigBatch]
@@ -1888,6 +1902,31 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
(channelReestablishAlice, channelReestablishBob)
}
+ private def reconnectWithLegacyPeer(f: FixtureParam, sendReestablish: Boolean = true): (ChannelReestablish, ChannelReestablish) = {
+ import f._
+
+ // Modify both nodes' state data so they see each other as using the legacy splice protocol.
+ // This must be done before INPUT_RECONNECTED because the channel_reestablish is constructed using the current state data.
+ Seq(alice, bob).foreach { node =>
+ val data = node.stateData.asInstanceOf[DATA_NORMAL]
+ val newData = data.modify(_.commitments.channelParams.remoteParams.initFeatures).using { features =>
+ features.remove(Features.Splicing).add(Features.SplicePrototype, FeatureSupport.Optional)
+ }
+ node.setState(node.stateName, newData)
+ }
+
+ // Use legacy features for reconnection so that updateFeatures preserves the legacy setting.
+ val baseFeatures = alice.commitments.localChannelParams.initFeatures
+ val legacyInit = Init(baseFeatures.remove(Features.Splicing).add(Features.SplicePrototype, FeatureSupport.Optional))
+ alice ! INPUT_RECONNECTED(alice2bob.ref, legacyInit, legacyInit)
+ bob ! INPUT_RECONNECTED(bob2alice.ref, legacyInit, legacyInit)
+ val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish]
+ if (sendReestablish) alice2bob.forward(bob)
+ val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish]
+ if (sendReestablish) bob2alice.forward(alice)
+ (channelReestablishAlice, channelReestablishBob)
+ }
+
test("disconnect (tx_complete not received)") { f =>
import f._
// Disconnection with one side sending commit_sig
@@ -1914,8 +1953,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
reconnect(f)
// Bob and Alice will exchange tx_abort because Bob did not receive Alice's tx_complete before the disconnect.
+ bob2alice.expectMsgType[ChannelReady]
+ bob2alice.forward(alice)
bob2alice.expectMsgType[TxAbort]
bob2alice.forward(alice)
+ alice2bob.expectMsgType[ChannelReady]
+ alice2bob.forward(bob)
alice2bob.expectMsgType[TxAbort]
alice2bob.forward(bob)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice)
@@ -1957,14 +2000,16 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
disconnect(f)
- // If Bob has not implemented https://github.com/lightning/bolts/pull/1214, he will send an incorrect next_commitment_number.
+ // If Bob has not implemented https://github.com/lightning/bolts/pull/1214, he will not ask for a retransmission of commit_sig.
val (channelReestablishAlice1, channelReestablishBob1) = reconnect(f, sendReestablish = false)
assert(channelReestablishAlice1.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
- assert(channelReestablishAlice1.nextLocalCommitmentNumber == aliceCommitIndex)
+ assert(channelReestablishAlice1.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishAlice1.nextLocalCommitmentNumber == aliceCommitIndex + 1)
assert(channelReestablishBob1.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
- assert(channelReestablishBob1.nextLocalCommitmentNumber == bobCommitIndex)
+ assert(channelReestablishBob1.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishBob1.nextLocalCommitmentNumber == bobCommitIndex + 1)
alice2bob.forward(bob, channelReestablishAlice1)
- bob2alice.forward(alice, channelReestablishBob1.copy(nextLocalCommitmentNumber = bobCommitIndex + 1))
+ bob2alice.forward(alice, channelReestablishBob1.copy(tlvStream = TlvStream(channelReestablishBob1.tlvStream.records.filterNot(_.isInstanceOf[ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv]))))
// In that case Alice won't retransmit commit_sig and the splice won't complete since they haven't exchanged tx_signatures.
assert(bob2alice.expectMsgType[CommitSig].fundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
bob2alice.forward(alice)
@@ -1978,13 +2023,15 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
alice ! cmd.copy(replyTo = probe.ref)
probe.expectMsgType[RES_ADD_FAILED[ForbiddenDuringSplice]]
- // But when correctly setting their next_commitment_number, they're able to finalize the splice.
+ // But when correctly setting their next_funding TLV, they're able to finalize the splice.
disconnect(f)
val (channelReestablishAlice2, channelReestablishBob2) = reconnect(f)
assert(channelReestablishAlice2.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
+ assert(!channelReestablishAlice2.retransmitInteractiveTxCommitSig)
assert(channelReestablishAlice2.nextLocalCommitmentNumber == aliceCommitIndex + 1)
assert(channelReestablishBob2.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
- assert(channelReestablishBob2.nextLocalCommitmentNumber == bobCommitIndex)
+ assert(channelReestablishBob2.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishBob2.nextLocalCommitmentNumber == bobCommitIndex + 1)
// Alice retransmits commit_sig and both retransmit tx_signatures.
assert(alice2bob.expectMsgType[CommitSig].fundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
@@ -2011,6 +2058,128 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
resolveHtlcs(f, htlcs)
}
+ test("disconnect (commit_sig not received) with legacy peer") { f =>
+ import f._
+
+ val htlcs = setupHtlcs(f)
+ val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex
+ val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex
+
+ val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey)))
+ alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig
+ bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig
+ awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs])
+ val spliceStatus = alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs]
+
+ disconnect(f)
+
+ val (channelReestablishAlice, channelReestablishBob) = reconnectWithLegacyPeer(f)
+
+ // Experimental protocol uses an experimental TLV.
+ assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
+ assert(channelReestablishAlice.tlvStream.get[ChannelReestablishTlv.ExperimentalNextFundingTlv].map(_.txId).contains(spliceStatus.signingSession.fundingTx.txId))
+ // Experimental protocol doesn't use the explicit retransmit flag.
+ assert(!channelReestablishAlice.retransmitInteractiveTxCommitSig)
+ // Experimental protocol rolls back nextLocalCommitmentNumber to signal commit_sig wasn't received.
+ assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex)
+ assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex)
+
+ // Legacy peers always retransmit channel_ready for the initial funding.
+ alice2bob.expectMsgType[ChannelReady]
+ alice2bob.forward(bob)
+ bob2alice.expectMsgType[ChannelReady]
+ bob2alice.forward(alice)
+
+ // Both sides retransmit commit_sig.
+ assert(alice2bob.expectMsgType[CommitSig].fundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
+ alice2bob.forward(bob)
+ assert(bob2alice.expectMsgType[CommitSig].fundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
+ bob2alice.forward(alice)
+ bob2alice.expectMsgType[TxSignatures]
+ bob2alice.forward(alice)
+ alice2bob.expectMsgType[TxSignatures]
+ alice2bob.forward(bob)
+ sender.expectMsgType[RES_SPLICE]
+
+ val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get
+ assert(spliceTx.txid == spliceStatus.signingSession.fundingTx.txId)
+ alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid)
+ bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid)
+ alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx)
+ alice2bob.expectMsgType[SpliceLocked]
+ alice2bob.forward(bob)
+ bob ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx)
+ bob2alice.expectMsgType[SpliceLocked]
+ bob2alice.forward(alice)
+ awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)
+ awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)
+
+ resolveHtlcs(f, htlcs)
+ }
+
+ test("re-send splice_locked for legacy peers") { f =>
+ import f._
+
+ val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)))
+ checkWatchConfirmed(f, fundingTx)
+
+ // Both sides confirm the splice and exchange splice_locked.
+ alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx)
+ alice2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == fundingTx.txid)
+ assert(alice2bob.expectMsgType[SpliceLocked].fundingTxId == fundingTx.txid)
+ alice2bob.forward(bob)
+ bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx)
+ bob2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == fundingTx.txid)
+ assert(bob2alice.expectMsgType[SpliceLocked].fundingTxId == fundingTx.txid)
+ bob2alice.forward(alice)
+ awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)
+ awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)
+
+ disconnect(f)
+ val (channelReestablishAlice, channelReestablishBob) = reconnectWithLegacyPeer(f)
+
+ // With the experimental protocol, peers retransmit splice_locked on reconnection.
+ assert(channelReestablishAlice.myCurrentFundingLocked_opt.contains(fundingTx.txid))
+ assert(channelReestablishBob.myCurrentFundingLocked_opt.contains(fundingTx.txid))
+ assert(!channelReestablishAlice.retransmitAnnSigs)
+ assert(!channelReestablishBob.retransmitAnnSigs)
+ assert(alice2bob.expectMsgType[SpliceLocked].fundingTxId == fundingTx.txid)
+ alice2bob.forward(bob)
+ assert(bob2alice.expectMsgType[SpliceLocked].fundingTxId == fundingTx.txid)
+ bob2alice.forward(alice)
+ }
+
+ test("don't re-send splice_locked on reconnection") { f =>
+ import f._
+
+ val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)))
+ checkWatchConfirmed(f, fundingTx)
+
+ // Both sides confirm the splice and exchange splice_locked.
+ alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx)
+ alice2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == fundingTx.txid)
+ assert(alice2bob.expectMsgType[SpliceLocked].fundingTxId == fundingTx.txid)
+ alice2bob.forward(bob)
+ bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx)
+ bob2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == fundingTx.txid)
+ assert(bob2alice.expectMsgType[SpliceLocked].fundingTxId == fundingTx.txid)
+ bob2alice.forward(alice)
+ awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)
+ awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)
+
+ alice2bob.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+ bob2alice.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+
+ disconnect(f)
+ val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
+
+ // No splice_locked or other messages are retransmitted: my_current_funding_locked implies splice_locked.
+ assert(channelReestablishAlice.myCurrentFundingLocked_opt.contains(fundingTx.txid))
+ assert(channelReestablishBob.myCurrentFundingLocked_opt.contains(fundingTx.txid))
+ alice2bob.expectNoMessage(100 millis)
+ bob2alice.expectNoMessage(100 millis)
+ }
+
test("disconnect (commit_sig not received, missing current nonce)", Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f =>
import f._
@@ -2124,9 +2293,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
disconnect(f)
val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTxId))
+ assert(!channelReestablishAlice.retransmitInteractiveTxCommitSig)
assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1)
assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTxId))
- assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex)
+ assert(channelReestablishBob.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1)
commitmentFormat match {
case _: SegwitV0CommitmentFormat => ()
case _: SimpleTaprootChannelCommitmentFormat =>
@@ -2194,8 +2365,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
disconnect(f)
val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
- assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex)
+ assert(channelReestablishAlice.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1)
assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
+ assert(!channelReestablishBob.retransmitInteractiveTxCommitSig)
assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1)
commitmentFormat match {
case _: SegwitV0CommitmentFormat => ()
@@ -2419,8 +2592,6 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// Alice retransmits tx_signatures.
alice2bob.expectMsgType[TxSignatures]
alice2bob.forward(bob)
- assert(alice2bob.expectMsgType[SpliceLocked].fundingTxId == spliceTx.txid)
- alice2bob.forward(bob)
bob2alice.expectNoMessage(100 millis)
bob ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx)
bob2alice.expectMsgType[SpliceLocked]
@@ -2453,13 +2624,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty)
assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceTx.txid))
+ bob2alice.expectMsgType[ChannelReady]
bob2alice.expectNoMessage(100 millis)
// Bob receives Alice's tx_signatures, which completes the splice.
alice2bob.expectMsgType[TxSignatures]
alice2bob.forward(bob)
- alice2bob.expectMsgType[SpliceLocked]
- alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice)
}
@@ -2532,9 +2702,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
disconnect(f)
val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId))
- assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex)
+ assert(channelReestablishAlice.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1)
assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId))
- assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex)
+ assert(channelReestablishBob.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1)
bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid)
// Alice and Bob retransmit commit_sig and tx_signatures.
@@ -2579,9 +2751,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
disconnect(f)
val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId))
+ assert(!channelReestablishAlice.retransmitInteractiveTxCommitSig)
assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1)
assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId))
- assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex)
+ assert(channelReestablishBob.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1)
bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid)
// Alice retransmits commit_sig, and they exchange tx_signatures afterwards.
@@ -2629,9 +2803,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
disconnect(f)
val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(rbfTxId))
- assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex)
+ assert(channelReestablishAlice.retransmitInteractiveTxCommitSig)
+ assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1)
assert(channelReestablishAlice.currentCommitNonce_opt.nonEmpty)
assert(channelReestablishBob.nextFundingTxId_opt.contains(rbfTxId))
+ assert(!channelReestablishBob.retransmitInteractiveTxCommitSig)
assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1)
assert(channelReestablishBob.currentCommitNonce_opt.isEmpty)
Seq(channelReestablishAlice, channelReestablishBob).foreach(channelReestablish => {
@@ -2742,16 +2918,15 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// From Alice's point of view, we now have two unconfirmed splices.
- alice2bob.ignoreMsg { case _: ChannelUpdate => true }
- bob2alice.ignoreMsg { case _: ChannelUpdate => true }
+ alice2bob.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+ bob2alice.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
disconnect(f)
- reconnect(f)
-
- // NB: channel_ready are not re-sent because the channel has already been used (for building splices).
- // Alice has already received `splice_locked` from Bob for the first splice, so he doesn't need to resend it.
- bob2alice.expectNoMessage(100 millis)
- alice2bob.expectNoMessage(100 millis)
+ val (channelReestablishA1, channelReestablishB1) = reconnect(f)
+ // Alice has locked the initial funding transaction, but not the splice transaction yet.
+ assert(channelReestablishA1.myCurrentFundingLocked_opt.nonEmpty)
+ assert(!channelReestablishA1.myCurrentFundingLocked_opt.contains(fundingTx1.txid))
+ assert(channelReestablishB1.myCurrentFundingLocked_opt.contains(fundingTx1.txid))
// The first splice confirms on Alice's side.
alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1)
@@ -2761,11 +2936,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
alice2blockchain.expectMsgTypeHaving[UnwatchFundingSpent](_.txId == fundingInput.txid)
disconnect(f)
- reconnect(f)
-
- // Alice and Bob have already exchanged `splice_locked` for the first splice, so there is need to resend it.
- bob2alice.expectNoMessage(100 millis)
- alice2bob.expectNoMessage(100 millis)
+ val (channelReestablishA2, channelReestablishB2) = reconnect(f)
+ assert(channelReestablishA2.myCurrentFundingLocked_opt.contains(fundingTx1.txid))
+ assert(channelReestablishB2.myCurrentFundingLocked_opt.contains(fundingTx1.txid))
// The second splice confirms on Alice's side.
alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx2)
@@ -2775,10 +2948,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
alice2blockchain.expectMsgTypeHaving[UnwatchFundingSpent](_.txId == fundingTx1.txid)
disconnect(f)
- reconnect(f)
-
- alice2bob.expectNoMessage(100 millis)
- bob2alice.expectNoMessage(100 millis)
+ val (channelReestablishA3, channelReestablishB3) = reconnect(f)
+ assert(channelReestablishA3.myCurrentFundingLocked_opt.contains(fundingTx2.txid))
+ assert(channelReestablishB3.myCurrentFundingLocked_opt.contains(fundingTx1.txid))
// The second splice confirms on Bob's side.
bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx2)
@@ -2788,14 +2960,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
// NB: we disconnect *before* transmitting the splice_locked to Alice.
disconnect(f)
- reconnect(f)
-
- alice2bob.expectNoMessage(100 millis)
- bob2alice.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == fundingTx2.txid)
- // This time alice received the splice_locked for the second splice.
- bob2alice.forward(alice)
- alice2bob.expectNoMessage(100 millis)
- bob2alice.expectNoMessage(100 millis)
+ val (channelReestablishA4, channelReestablishB4) = reconnect(f)
+ assert(channelReestablishA4.myCurrentFundingLocked_opt.contains(fundingTx2.txid))
+ assert(channelReestablishB4.myCurrentFundingLocked_opt.contains(fundingTx2.txid))
disconnect(f)
reconnect(f)
@@ -2873,6 +3040,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
alice2bob.expectNoMessage(100 millis)
bob2alice.expectNoMessage(100 millis)
+ alice2bob.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+ bob2alice.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+
// Bob will not receive Alice's tx_signatures, update_add_htlc or commit_sigs before disconnecting.
disconnect(f)
reconnect(f)
@@ -2911,8 +3081,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
alice2bob.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == fundingTx.txid)
alice2bob.forward(bob)
- alice2bob.ignoreMsg { case _: ChannelUpdate => true }
- bob2alice.ignoreMsg { case _: ChannelUpdate => true }
+ alice2bob.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+ bob2alice.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
disconnect(f)
@@ -2926,19 +3096,15 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.active.size == 2)
assert(bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.active.size == 1)
- reconnect(f)
+ val (channelReestablishA, channelReestablishB) = reconnect(f)
+ assert(channelReestablishA.myCurrentFundingLocked_opt.contains(fundingTx.txid))
+ assert(channelReestablishB.myCurrentFundingLocked_opt.contains(fundingTx.txid))
- // Because `your_last_funding_locked_txid` from Bob matches the last `splice_locked` txid sent by Alice; there is no need
- // for Alice to resend `splice_locked`. Alice processes the `my_current_funding_locked` from Bob as if she received
- // `splice_locked` from Bob and prunes the initial funding commitment.
+ // Alice processes the `my_current_funding_locked` from Bob as if she received `splice_locked` from Bob and prunes the initial funding commitment.
awaitCond(alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.active.size == 1)
assert(alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.active.head.fundingTxId == fundingTx.txid)
alice2bob.expectNoMessage(100 millis)
- // The `your_last_funding_locked_txid` from Alice does not match the last `splice_locked` sent by Bob, so Bob must resend `splice_locked`.
- val bobSpliceLocked = bob2alice.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == fundingTx.txid)
- assert(bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.active.size == 1)
-
// Alice sends an HTLC before receiving Bob's splice_locked: see https://github.com/lightning/bolts/issues/1223.
addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice)
val sender = TestProbe()
@@ -2946,7 +3112,6 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
sender.expectMsgType[RES_SUCCESS[CMD_SIGN]]
assert(alice2bob.expectMsgType[CommitSig].fundingTxId_opt.contains(fundingTx.txid))
alice2bob.forward(bob)
- bob2alice.forward(alice, bobSpliceLocked)
bob2alice.expectMsgType[RevokeAndAck]
bob2alice.forward(alice)
assert(bob2alice.expectMsgType[CommitSig].fundingTxId_opt.contains(fundingTx.txid))
@@ -3055,6 +3220,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
alice2bob.expectNoMessage(100 millis)
bob2alice.expectNoMessage(100 millis)
+ alice2bob.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+ bob2alice.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+
// Bob will not receive Alice's commit_sigs before disconnecting.
disconnect(f)
val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
@@ -3125,6 +3293,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
alice2bob.expectNoMessage(100 millis)
bob2alice.expectNoMessage(100 millis)
+ alice2bob.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+ bob2alice.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+
// Alice will not receive Bob's commit_sigs before disconnecting.
disconnect(f)
val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
@@ -3192,6 +3363,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
alice2bob.expectNoMessage(100 millis)
bob2alice.expectNoMessage(100 millis)
+ alice2bob.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+ bob2alice.ignoreMsg { case _: ChannelUpdate | _: ChannelReady => true }
+
// Alice will not receive Bob's commit_sigs before disconnecting.
disconnect(f)
val (channelReestablishAlice, channelReestablishBob) = reconnect(f)
@@ -3232,40 +3406,23 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
bob2alice.forward(alice)
// Alice sends announcement_signatures to Bob.
- alice2bob.expectMsgType[AnnouncementSignatures]
+ inside(alice2bob.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, fundingTx).contains(ann.shortChannelId)) }
alice2bob.forward(bob)
// Alice disconnects before Bob can send announcement_signatures.
- bob2alice.expectMsgType[AnnouncementSignatures]
+ inside(bob2alice.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, fundingTx).contains(ann.shortChannelId)) }
disconnect(f)
- reconnect(f)
-
- // Bob will not resend `splice_locked` because he has already received `announcement_signatures` from Alice.
- bob2alice.expectNoMessage(100 millis)
-
- // Alice resends `splice_locked` because she did not receive `announcement_signatures` from Bob before the disconnect.
- val aliceSpliceLocked = alice2bob.expectMsgType[SpliceLocked]
- alice2bob.forward(bob)
- alice2bob.expectNoMessage(100 millis)
-
- // Bob receives Alice's `splice_locked` after `channel_reestablish` and must retransmit both `splice_locked` and `announcement_signatures`.
- val bobSpliceLocked = bob2alice.expectMsgType[SpliceLocked]
- bob2alice.forward(alice)
- bob2alice.expectMsgType[AnnouncementSignatures]
+ val (channelReestablishA, channelReestablishB) = reconnect(f)
+ assert(channelReestablishA.retransmitAnnSigs)
+ assert(!channelReestablishB.retransmitAnnSigs)
+ inside(bob2alice.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, fundingTx).contains(ann.shortChannelId)) }
bob2alice.forward(alice)
bob2alice.expectNoMessage(100 millis)
-
- // Alice retransmits `announcement_signatures` to Bob after receiving `splice_locked` from Bob.
- alice2bob.expectMsgType[AnnouncementSignatures]
- alice2bob.forward(bob)
alice2bob.expectNoMessage(100 millis)
- bob2alice.expectNoMessage(100 millis)
- // If either node receives `splice_locked` again, it should be ignored; `announcement_signatures have already been sent.
- alice2bob.forward(bob, aliceSpliceLocked)
- bob2alice.forward(alice, bobSpliceLocked)
- alice2bob.expectNoMessage(100 millis)
+ // If Bob receives `splice_locked` again, it should be ignored; `announcement_signatures have already been sent.
+ alice2bob.forward(bob, SpliceLocked(alice.commitments.channelId, fundingTx.txid))
bob2alice.expectNoMessage(100 millis)
// the splice is locked on both sides
@@ -3279,41 +3436,39 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
}
}
- test("disconnect before receiving splice_locked from a legacy peer") { f =>
+ test("disconnect before receiving announcement_signatures from one peer (splice locked on one side only)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f =>
import f._
val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)))
checkWatchConfirmed(f, fundingTx)
- // The splice confirms for both.
- alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx)
+ // The splice confirms on Alice's side.
+ alice ! WatchFundingConfirmedTriggered(BlockHeight(420000), 42, fundingTx)
alice2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == fundingTx.txid)
alice2bob.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == fundingTx.txid)
alice2bob.forward(bob)
- bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx)
- bob2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == fundingTx.txid)
- bob2alice.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == fundingTx.txid)
- bob2alice.forward(alice)
-
- alice2bob.ignoreMsg { case _: ChannelUpdate => true }
- bob2alice.ignoreMsg { case _: ChannelUpdate => true }
+ alice2bob.expectNoMessage(100 millis)
+ // The splice confirms on Bob's side while offline.
disconnect(f)
- val (aliceReestablish, bobReestablish) = reconnect(f, sendReestablish = false)
+ bob ! WatchFundingConfirmedTriggered(BlockHeight(420000), 42, fundingTx)
+ bob2blockchain.expectMsgTypeHaving[WatchFundingSpent](_.txId == fundingTx.txid)
- // remove the last_funding_locked tlv from the reestablish messages
- alice2bob.forward(bob, aliceReestablish.copy(tlvStream = TlvStream.empty))
- bob2alice.forward(alice, bobReestablish.copy(tlvStream = TlvStream.empty))
+ // On reconnection, both nodes want to exchange announcement_signatures for the splice.
+ val (channelReestablishA, channelReestablishB) = reconnect(f)
+ assert(channelReestablishA.retransmitAnnSigs)
+ assert(channelReestablishA.myCurrentFundingLocked_opt.contains(fundingTx.txid))
+ assert(channelReestablishB.retransmitAnnSigs)
+ assert(channelReestablishB.myCurrentFundingLocked_opt.contains(fundingTx.txid))
- // always send last splice_locked after reconnection if the last_funding_locked tlv is not set
- alice2bob.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == fundingTx.txid)
- bob2alice.expectMsgTypeHaving[SpliceLocked](_.fundingTxId == fundingTx.txid)
+ inside(alice2bob.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, fundingTx).contains(ann.shortChannelId)) }
alice2bob.forward(bob)
+ inside(bob2alice.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, fundingTx).contains(ann.shortChannelId)) }
bob2alice.forward(alice)
- alice2bob.expectNoMessage(100 millis)
bob2alice.expectNoMessage(100 millis)
+ alice2bob.expectNoMessage(100 millis)
- // the splice is locked on both sides
+ // The splice is locked on both sides.
alicePeer.fishForMessage() {
case e: ChannelReadyForPayments => e.fundingTxIndex == 1
case _ => false
@@ -3344,38 +3499,24 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
bob2alice.forward(alice)
// Alice sends announcement_signatures to Bob.
- alice2bob.expectMsgType[AnnouncementSignatures]
-
+ inside(alice2bob.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, fundingTx).contains(ann.shortChannelId)) }
// Bob sends announcement_signatures to Alice.
- bob2alice.expectMsgType[AnnouncementSignatures]
+ inside(bob2alice.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, fundingTx).contains(ann.shortChannelId)) }
disconnect(f)
reconnect(f)
- // Bob resends `splice_locked` because he did not receive `announcement_signatures` from Alice before the disconnect.
- val bobSpliceLocked = bob2alice.expectMsgType[SpliceLocked]
- bob2alice.expectNoMessage(100 millis)
-
- // Alice resends `splice_locked` because she did not receive `announcement_signatures` from Bob before the disconnect.
- val aliceSpliceLocked = alice2bob.expectMsgType[SpliceLocked]
+ inside(alice2bob.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, fundingTx).contains(ann.shortChannelId)) }
alice2bob.forward(bob)
alice2bob.expectNoMessage(100 millis)
- // Alice receives Bob's `splice_locked` after already resending their `splice_locked` and retransmits `announcement_signatures`.
+ inside(bob2alice.expectMsgType[AnnouncementSignatures]) { ann => assert(getFundingScid(f, fundingTx).contains(ann.shortChannelId)) }
bob2alice.forward(alice)
- alice2bob.expectMsgType[AnnouncementSignatures]
- alice2bob.forward(bob)
- alice2bob.expectNoMessage(100 millis)
-
- // Bob retransmits `announcement_signatures` to Alice after receiving `announcement_signatures` from Alice.
- bob2alice.expectMsgType[AnnouncementSignatures]
- bob2alice.forward(alice)
- alice2bob.expectNoMessage(100 millis)
bob2alice.expectNoMessage(100 millis)
// If either node receives `splice_locked` again, it should be ignored; `announcement_signatures have already been sent.
- alice2bob.forward(bob, aliceSpliceLocked)
- bob2alice.forward(alice, bobSpliceLocked)
+ alice2bob.forward(bob, SpliceLocked(alice.commitments.channelId, fundingTx.txid))
+ bob2alice.forward(alice, SpliceLocked(bob.commitments.channelId, fundingTx.txid))
alice2bob.expectNoMessage(100 millis)
bob2alice.expectNoMessage(100 millis)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala
index 5ce9c41..2e69a3d 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala
@@ -71,9 +71,12 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
}
}
- private def lastFundingLockedTlvs(commitments: Commitments): Set[ChannelReestablishTlv] =
- commitments.lastLocalLocked_opt.map(c => ChannelReestablishTlv.MyCurrentFundingLockedTlv(c.fundingTxId)).toSet ++
- commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv.YourLastFundingLockedTlv(c.fundingTxId)).toSet
+ private def lastFundingLockedTlvs(commitments: Commitments): Set[ChannelReestablishTlv] = {
+ commitments.lastLocalLocked_opt.map(c => {
+ val retransmitAnnSigs = commitments.announceChannel
+ ChannelReestablishTlv.MyCurrentFundingLockedTlv(c.fundingTxId, retransmitAnnSigs)
+ }).toSet
+ }
test("reconnect after creating channel", Tag(IgnoreChannelUpdates)) { f =>
import f._
@@ -120,10 +123,12 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
disconnect(alice, bob)
val (aliceCurrentPerCommitmentPoint, bobCurrentPerCommitmentPoint) = reconnect(alice, bob, alice2bob, bob2alice)
- val reestablishA = alice2bob.expectMsg(ChannelReestablish(htlc.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), aliceCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(alice.stateData.asInstanceOf[DATA_NORMAL].commitments))))
- val reestablishB = bob2alice.expectMsg(ChannelReestablish(htlc.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(bob.stateData.asInstanceOf[DATA_NORMAL].commitments))))
+ val reestablishA = alice2bob.expectMsg(ChannelReestablish(htlc.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), aliceCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(alice.commitments))))
+ val reestablishB = bob2alice.expectMsg(ChannelReestablish(htlc.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(bob.commitments))))
alice2bob.forward(bob, reestablishA)
bob2alice.forward(alice, reestablishB)
+ alice2bob.expectMsgType[ChannelReady]
+ bob2alice.expectMsgType[ChannelReady]
// alice will re-send the update and the sig
alice2bob.expectMsg(htlc)
@@ -175,8 +180,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
disconnect(alice, bob)
val (aliceCurrentPerCommitmentPoint, bobCurrentPerCommitmentPoint) = reconnect(alice, bob, alice2bob, bob2alice)
- val reestablishA = alice2bob.expectMsg(ChannelReestablish(htlc.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), aliceCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(alice.stateData.asInstanceOf[DATA_NORMAL].commitments))))
- val reestablishB = bob2alice.expectMsg(ChannelReestablish(htlc.channelId, 2, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(bob.stateData.asInstanceOf[DATA_NORMAL].commitments))))
+ val reestablishA = alice2bob.expectMsg(ChannelReestablish(htlc.channelId, 1, 0, PrivateKey(ByteVector32.Zeroes), aliceCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(alice.commitments))))
+ val reestablishB = bob2alice.expectMsg(ChannelReestablish(htlc.channelId, 2, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(bob.commitments))))
alice2bob.forward(bob, reestablishA)
bob2alice.forward(alice, reestablishB)
@@ -223,8 +228,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
{
val (aliceCurrentPerCommitmentPoint, bobCurrentPerCommitmentPoint) = reconnect(alice, bob, alice2bob, bob2alice)
- val reestablishA = alice2bob.expectMsg(ChannelReestablish(htlc.channelId, 1, 1, revB.perCommitmentSecret, aliceCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(alice.stateData.asInstanceOf[DATA_NORMAL].commitments))))
- val reestablishB = bob2alice.expectMsg(ChannelReestablish(htlc.channelId, 2, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(bob.stateData.asInstanceOf[DATA_NORMAL].commitments))))
+ val reestablishA = alice2bob.expectMsg(ChannelReestablish(htlc.channelId, 1, 1, revB.perCommitmentSecret, aliceCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(alice.commitments))))
+ val reestablishB = bob2alice.expectMsg(ChannelReestablish(htlc.channelId, 2, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(bob.commitments))))
alice2bob.forward(bob, reestablishA)
bob2alice.forward(alice, reestablishB)
}
@@ -254,8 +259,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
{
val (aliceCurrentPerCommitmentPoint, bobCurrentPerCommitmentPoint) = reconnect(alice, bob, alice2bob, bob2alice)
- val reestablishA = alice2bob.expectMsg(ChannelReestablish(htlc.channelId, 2, 1, revB.perCommitmentSecret, aliceCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(alice.stateData.asInstanceOf[DATA_NORMAL].commitments))))
- val reestablishB = bob2alice.expectMsg(ChannelReestablish(htlc.channelId, 2, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(bob.stateData.asInstanceOf[DATA_NORMAL].commitments))))
+ val reestablishA = alice2bob.expectMsg(ChannelReestablish(htlc.channelId, 2, 1, revB.perCommitmentSecret, aliceCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(alice.commitments))))
+ val reestablishB = bob2alice.expectMsg(ChannelReestablish(htlc.channelId, 2, 0, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint, TlvStream(lastFundingLockedTlvs(bob.commitments))))
alice2bob.forward(bob, reestablishA)
bob2alice.forward(alice, reestablishB)
}
@@ -715,21 +720,23 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
reconnect(alice, bob, alice2bob, bob2alice)
// Alice and Bob exchange channel_reestablish and channel_ready again.
- alice2bob.expectMsgType[ChannelReestablish]
- bob2alice.expectMsgType[ChannelReestablish]
+ assert(!alice2bob.expectMsgType[ChannelReestablish].retransmitAnnSigs)
+ assert(bob2alice.expectMsgType[ChannelReestablish].retransmitAnnSigs)
bob2alice.forward(alice)
alice2bob.forward(bob)
- alice2bob.expectNoMessage(100 millis)
-
- // Bob retransmits his channel_ready and announcement_signatures because he hasn't received Alice's announcement_signatures.
+ alice2bob.expectMsgType[ChannelReady]
+ alice2bob.forward(bob)
bob2alice.expectMsgType[ChannelReady]
bob2alice.forward(alice)
+ // Alice retransmits announcement_signatures because Bob requested it.
+ val annSigsAlice = alice2bob.expectMsgType[AnnouncementSignatures]
+ alice2bob.forward(bob)
+ alice2bob.expectNoMessage(100 millis)
+
+ // Bob retransmits announcement_signatures because he hasn't received Alice's announcement_signatures.
val annSigsBob = bob2alice.expectMsgType[AnnouncementSignatures]
bob2alice.forward(alice, annSigsBob)
- // Alice retransmits her announcement_signatures when receiving Bob's.
- val annSigsAlice = alice2bob.expectMsgType[AnnouncementSignatures]
- alice2bob.forward(bob, annSigsAlice)
// Alice and Bob ignore redundant announcement_signatures.
alice2bob.forward(bob, annSigsAlice)
bob2alice.expectNoMessage(100 millis)
@@ -749,6 +756,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.expectMsgType[ChannelReestablish]
bob2alice.forward(alice)
alice2bob.forward(bob)
+ alice2bob.expectMsgType[ChannelReady]
+ bob2alice.expectMsgType[ChannelReady]
// alice and bob resend their channel update at reconnection (unannounced channel)
alice2bob.expectMsgType[ChannelUpdate]
@@ -907,8 +916,6 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.expectMsgType[ChannelReestablish]
bob2alice.forward(alice)
alice2bob.forward(bob)
- bob2alice.expectMsgType[ChannelReady]
- bob2alice.forward(alice)
// Alice will NOT resend their channel_ready at reconnection because she has received bob's announcement_signatures (pre-splice behavior).
alice2bob.expectNoMessage(100 millis)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala
index 24bcd32..1eb6eab 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/channel/GossipIntegrationSpec.scala
@@ -25,9 +25,7 @@ class GossipIntegrationSpec extends FixtureSpec with IntegrationPatience {
override def createFixture(testData: TestData): FixtureParam = {
// seeds have been chosen so that node ids start with 02aaaa for alice, 02bbbb for bob, etc.
val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3"))
- .modify(_.features).using(_.add(Features.SplicePrototype, FeatureSupport.Optional))
val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492"))
- .modify(_.features).using(_.add(Features.SplicePrototype, FeatureSupport.Optional))
val carolParams = nodeParamsFor("carol", ByteVector32(hex"ebd5a5d3abfb3ef73731eb3418d918f247445183180522674666db98a66411cc"))
ThreeNodesFixture(aliceParams, bobParams, carolParams, testData.name)
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala
index bb050bf..b8badd7 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala
@@ -118,7 +118,9 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
test("add liquidity if on-the-fly funding is used", Tag(noPlugin)) { f =>
import f._
- val features = defaultFeatures.add(Features.SplicePrototype, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional)
+ val features = defaultFeatures
+ .add(Features.Splicing, FeatureSupport.Optional)
+ .add(Features.OnTheFlyFunding, FeatureSupport.Optional)
val requestFunding = LiquidityAds.RequestFunding(250_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(randomBytes32() :: Nil))
val open = createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Some(requestFunding)).copy(
channelFlags = ChannelFlags(nonInitiatorPaysCommitFees = true, announceChannel = false),
@@ -210,7 +212,9 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
test("reject on-the-fly channel if another channel exists", Tag(noPlugin)) { f =>
import f._
- val features = defaultFeatures.add(Features.SplicePrototype, FeatureSupport.Optional).add(Features.OnTheFlyFunding, FeatureSupport.Optional)
+ val features = defaultFeatures
+ .add(Features.Splicing, FeatureSupport.Optional)
+ .add(Features.OnTheFlyFunding, FeatureSupport.Optional)
val requestFunding = LiquidityAds.RequestFunding(250_000 sat, TestConstants.defaultLiquidityRates.fundingRates.head, LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(randomBytes32() :: Nil))
val open = createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Some(requestFunding)).copy(
channelFlags = ChannelFlags(nonInitiatorPaysCommitFees = true, announceChannel = false),
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala
index 598bc1e..cc7904d 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala
@@ -19,11 +19,12 @@ package fr.acinq.eclair.io
import akka.actor.PoisonPill
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
-import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32}
+import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, OutPoint, SatoshiLong, Transaction, TxId}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.TestConstants._
import fr.acinq.eclair.TestUtils.randomTxId
+import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature
import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.io.Peer.ConnectionDown
@@ -155,7 +156,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"0000 00050100000000".bits).require.value)
transport.expectMsgType[TransportHandler.ReadAck]
probe.expectTerminated(transport.ref)
- origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features (unknown_32,var_onion_optin)"))
+ origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features (unknown_32,option_static_remotekey)"))
peer.expectMsg(ConnectionDown(peerConnection))
}
@@ -172,7 +173,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"00050100000000 0000".bits).require.value)
transport.expectMsgType[TransportHandler.ReadAck]
probe.expectTerminated(transport.ref)
- origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features (unknown_32,var_onion_optin)"))
+ origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features (unknown_32,option_static_remotekey)"))
peer.expectMsg(ConnectionDown(peerConnection))
}
@@ -723,5 +724,53 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
}
}
+ test("convert experimental splice messages") { f =>
+ import f._
+ val remoteInit = protocol.Init(Bob.nodeParams.features.initFeatures().add(Features.SplicePrototype, FeatureSupport.Optional))
+ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, remoteInit)
+
+ val spliceInit = SpliceInit(randomBytes32(), 100_000 sat, FeeratePerKw(5000 sat), 0, randomKey().publicKey)
+ val spliceAck = SpliceAck(randomBytes32(), 50_000 sat, randomKey().publicKey)
+ val spliceLocked = SpliceLocked(randomBytes32(), TxId(randomBytes32()))
+
+ // Outgoing messages use the experimental version of splicing.
+ peer.send(peerConnection, spliceInit)
+ transport.expectMsg(ExperimentalSpliceInit.from(spliceInit))
+ peer.send(peerConnection, spliceAck)
+ transport.expectMsg(ExperimentalSpliceAck.from(spliceAck))
+ peer.send(peerConnection, spliceLocked)
+ transport.expectMsg(ExperimentalSpliceLocked.from(spliceLocked))
+
+ // Incoming messages are converted from their experimental version.
+ transport.send(peerConnection, ExperimentalSpliceInit.from(spliceInit))
+ peer.expectMsg(spliceInit)
+ transport.expectMsgType[TransportHandler.ReadAck]
+ transport.send(peerConnection, ExperimentalSpliceAck.from(spliceAck))
+ peer.expectMsg(spliceAck)
+ transport.expectMsgType[TransportHandler.ReadAck]
+ transport.send(peerConnection, ExperimentalSpliceLocked.from(spliceLocked))
+ peer.expectMsg(spliceLocked)
+ transport.expectMsgType[TransportHandler.ReadAck]
+
+ // Incompatible TLVs are dropped when sending messages to peers using the experimental version.
+ val txAddInput = TxAddInput(randomBytes32(), UInt64(0), OutPoint(TxId(randomBytes32()), 3), 0)
+ assert(txAddInput.tlvStream.get[TxAddInputTlv.SharedInputTxId].nonEmpty)
+ peer.send(peerConnection, txAddInput)
+ assert(transport.expectMsgType[TxAddInput].tlvStream.get[TxAddInputTlv.SharedInputTxId].isEmpty)
+ val txSignatures = TxSignatures(randomBytes32(), Transaction(2, Nil, Nil, 0), Nil, Some(IndividualSignature(randomBytes64())))
+ assert(txSignatures.tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].nonEmpty)
+ peer.send(peerConnection, txSignatures)
+ assert(transport.expectMsgType[TxSignatures].tlvStream.get[TxSignaturesTlv.PreviousFundingTxSig].isEmpty)
+ val channelId = randomBytes32()
+ val commitSigBatch = CommitSigBatch(Seq(
+ CommitSig(channelId, IndividualSignature(randomBytes64()), Nil, TlvStream(CommitSigTlv.FundingTx(TxId(randomBytes32())), CommitSigTlv.ExperimentalBatchTlv(2))),
+ CommitSig(channelId, IndividualSignature(randomBytes64()), Nil, TlvStream(CommitSigTlv.FundingTx(TxId(randomBytes32())), CommitSigTlv.ExperimentalBatchTlv(2))),
+ ))
+ peer.send(peerConnection, commitSigBatch)
+ assert(transport.expectMsgType[CommitSig].tlvStream.get[CommitSigTlv.FundingTx].isEmpty)
+ assert(transport.expectMsgType[CommitSig].tlvStream.get[CommitSigTlv.FundingTx].isEmpty)
+ transport.expectNoMessage(100 millis)
+ }
+
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala
index b70f6f5..b523f57 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala
@@ -51,13 +51,13 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
Features.StaticRemoteKey -> FeatureSupport.Optional,
Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional,
Features.DualFunding -> FeatureSupport.Optional,
- Features.SplicePrototype -> FeatureSupport.Optional,
+ Features.Splicing -> FeatureSupport.Optional,
Features.OnTheFlyFunding -> FeatureSupport.Optional,
)
val remoteFeaturesWithFeeCredit = Features(
Features.DualFunding -> FeatureSupport.Optional,
- Features.SplicePrototype -> FeatureSupport.Optional,
+ Features.Splicing -> FeatureSupport.Optional,
Features.OnTheFlyFunding -> FeatureSupport.Optional,
Features.FundingFeeCredit -> FeatureSupport.Optional,
)
@@ -183,7 +183,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val nodeParams = TestConstants.Alice.nodeParams
.modify(_.features.activated).using(_ + (Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Optional))
.modify(_.features.activated).using(_ + (Features.DualFunding -> FeatureSupport.Optional))
- .modify(_.features.activated).using(_ + (Features.SplicePrototype -> FeatureSupport.Optional))
+ .modify(_.features.activated).using(_ + (Features.Splicing -> FeatureSupport.Optional))
.modify(_.features.activated).using(_ + (Features.OnTheFlyFunding -> FeatureSupport.Optional))
.modify(_.features.activated).usingIf(test.tags.contains(withFeeCredit))(_ + (Features.FundingFeeCredit -> FeatureSupport.Optional))
val remoteNodeId = randomKey().publicKey
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
index 88ed2d1..3db9f6f 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
@@ -159,9 +159,16 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
hex"0023" ++ channelId ++ signature ++ hex"fe47010000 07 cccccccccccccc" -> FundingSigned(channelId, signature, TlvStream[FundingSignedTlv](Set.empty[FundingSignedTlv], Set(GenericTlv(tlvTag, hex"cccccccccccccc")))),
hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point),
- hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"00 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextFundingTlv(txId))),
- hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"01 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.YourLastFundingLockedTlv(txId))),
- hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"03 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.MyCurrentFundingLockedTlv(txId))),
+ hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"00 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.ExperimentalNextFundingTlv(txId))),
+ // TODO: replace those test vectors with the commented ones below when we remove support for the legacy splicing protocol.
+ hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"01 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv(txId.value.reverse))),
+ hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"01 21" ++ txId.value.reverse ++ hex"00" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv(txId.value.reverse ++ hex"00"))),
+ hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"01 21" ++ txId.value.reverse ++ hex"01" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv(txId.value.reverse ++ hex"01"))),
+ // hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"01 21" ++ txId.value.reverse ++ hex"00" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextFundingTlv(txId, retransmitCommitSig = false))),
+ // hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"01 21" ++ txId.value.reverse ++ hex"01" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextFundingTlv(txId, retransmitCommitSig = true))),
+ hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"03 20" ++ txId.value.reverse -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.ExperimentalMyCurrentFundingLockedTlv(txId))),
+ hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"05 21" ++ txId.value.reverse ++ hex"00" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.MyCurrentFundingLockedTlv(txId, retransmitAnnSigs = false))),
+ hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"05 21" ++ txId.value.reverse ++ hex"01" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.MyCurrentFundingLockedTlv(txId, retransmitAnnSigs = true))),
hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"18 42" ++ nonce.data -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.CurrentCommitNonceTlv(nonce))),
hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"16 c4" ++ txId.value.reverse ++ nonce.data ++ nextTxId.value.reverse ++ nextNonce.data -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream(ChannelReestablishTlv.NextLocalNoncesTlv(Seq(txId -> nonce, nextTxId -> nextNonce)))),
hex"0088" ++ channelId ++ hex"0001020304050607 0809aabbccddeeff" ++ key.value ++ point.value ++ hex"fe47010000 00" -> ChannelReestablish(channelId, 0x01020304050607L, 0x0809aabbccddeeffL, key, point, TlvStream[ChannelReestablishTlv](Set.empty[ChannelReestablishTlv], Set(GenericTlv(tlvTag, ByteVector.empty)))),
@@ -222,7 +229,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
TxAddInput(channelId1, UInt64(561), Some(tx1), 1, 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005",
TxAddInput(channelId2, UInt64(0), Some(tx2), 2, 0) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000",
TxAddInput(channelId1, UInt64(561), Some(tx1), 0, 0) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000",
- TxAddInput(channelId1, UInt64(561), OutPoint(tx1, 1), 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106",
+ TxAddInput(channelId1, UInt64(561), OutPoint(tx1, 1), 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 00201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106",
TxAddInput(channelId1, UInt64(561), None, 1, 0xfffffffdL, TlvStream(TxAddInputTlv.PrevTxOut(tx2.txid, 22_549_834 sat, hex"00148d2e0b57adcb8869e603fd35b5179caf05336125"))) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 fffffffd fd04573efc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1000000000158154a00148d2e0b57adcb8869e603fd35b5179caf05336125",
TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472",
TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Set.empty[TxAddOutputTlv], Set(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a",
@@ -234,7 +241,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
TxComplete(channelId1, TlvStream(Set.empty[TxCompleteTlv], Set(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00",
TxSignatures(channelId1, tx2, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87")), ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484",
TxSignatures(channelId2, tx1, Nil, None) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000",
- TxSignatures(channelId2, tx1, Nil, Some(IndividualSignature(signature))) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ TxSignatures(channelId2, tx1, Nil, Some(IndividualSignature(signature))) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 0040aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025940aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
TxSignatures(channelId2, tx1, Nil, Some(PartialSignatureWithNonce(partialSig, fundingNonce))) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 02 62 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb a49ff67b08c720b993c946556cde1be1c3b664bc847c4792135dfd6ef0986e00e9871808c6620b0420567dad525b27431453d4434fd326f8ac56496639b72326eb5d",
TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0",
TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), 1_500_000 sat, requireConfirmedInputs = true, None) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360 0200",
@@ -414,29 +421,30 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
val channelId = ByteVector32(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
val fundingTxId = TxId(TxHash(ByteVector32(hex"24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566")))
val fundingPubkey = PublicKey(hex"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
- val fundingRate = LiquidityAds.FundingRate(100_000.sat, 100_000.sat, 400, 150, 0.sat, 0.sat)
+ val fundingRate = LiquidityAds.FundingRate(100_000 sat, 100_000 sat, 400, 150, 0 sat, 0 sat)
val testCases = Seq(
// @formatter:off
- SpliceInit(channelId, 100_000 sat, FeeratePerKw(2500 sat), 100, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
- SpliceInit(channelId, 150_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 25_000_000 msat, requireConfirmedInputs = false, None) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000249f0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000704017d7840",
- SpliceInit(channelId, 0 sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
- SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
- SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.RequestFunding(100_000 sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance))) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1e00000000000186a0000186a0000186a00190009600000000000000000000",
- SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, None, Some(SimpleTaprootChannelsPhoenix)) -> hex"9088 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c400000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe47000011 47 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000",
- SpliceAck(channelId, 25_000 sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
- SpliceAck(channelId, 40_000 sat, fundingPubkey, 10_000_000 msat, requireConfirmedInputs = false, None, None) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000703989680",
- SpliceAck(channelId, 0 sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
- SpliceAck(channelId, (-25_000).sat, fundingPubkey) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff9e58 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
- SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.WillFund(fundingRate, hex"deadbeef", ByteVector64.Zeroes)), None) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b5a000186a0000186a00190009600000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
- SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(0 msat))) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda05200",
- SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(1729 msat))) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda0520206c1",
- SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, None, None, Some(SimpleTaprootChannelsPhoenix)) -> hex"908a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe47000011 47 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000", SpliceLocked(channelId, fundingTxId) -> hex"908c aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566",
+ SpliceInit(channelId, 100_000 sat, FeeratePerKw(2500 sat), 100, fundingPubkey) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ SpliceInit(channelId, 150_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 25_000_000 msat, requireConfirmedInputs = false, None) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000249f0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000704017d7840",
+ SpliceInit(channelId, 0 sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ SpliceInit(channelId, (-50_000).sat, FeeratePerKw(500 sat), 0, fundingPubkey) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff3cb0 000001f4 00000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.RequestFunding(100_000 sat, fundingRate, LiquidityAds.PaymentDetails.FromChannelBalance))) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c4 00000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b1e00000000000186a0000186a0000186a00190009600000000000000000000",
+ SpliceInit(channelId, 100_000 sat, 100, FeeratePerKw(2500 sat), fundingPubkey, 0 msat, requireConfirmedInputs = false, None, Some(SimpleTaprootChannelsPhoenix)) -> hex"0050 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000186a0 000009c400000064 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe47000011 47 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000",
+ SpliceAck(channelId, 25_000 sat, fundingPubkey) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ SpliceAck(channelId, 40_000 sat, fundingPubkey, 10_000_000 msat, requireConfirmedInputs = false, None, None) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000009c40 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe4700000703989680",
+ SpliceAck(channelId, 0 sat, fundingPubkey) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000000 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ SpliceAck(channelId, (-25_000).sat, fundingPubkey) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ffffffffffff9e58 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, Some(LiquidityAds.WillFund(fundingRate, hex"deadbeef", ByteVector64.Zeroes)), None) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fd053b5a000186a0000186a00190009600000000000000000004deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(0 msat))) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda05200",
+ SpliceAck(channelId, 25_000 sat, fundingPubkey, TlvStream(ChannelTlv.FeeCreditUsedTlv(1729 msat))) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fda0520206c1",
+ SpliceAck(channelId, 25_000 sat, fundingPubkey, 0 msat, requireConfirmedInputs = false, None, None, Some(SimpleTaprootChannelsPhoenix)) -> hex"0051 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000000061a8 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 fe47000011 47 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000",
+ SpliceLocked(channelId, fundingTxId) -> hex"004d aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 24e1b2c94c4e734dd5b9c5f3c910fbb6b3b436ced6382c7186056a5a23f14566",
// @formatter:on
)
- testCases.foreach { case (message, bin) =>
+ testCases.foreach { case (msg, bin) =>
val decoded = lightningMessageCodec.decode(bin.bits).require.value
- assert(decoded == message)
- val encoded = lightningMessageCodec.encode(message).require.bytes
+ assert(decoded == msg)
+ val encoded = lightningMessageCodec.encode(msg).require.bytes
assert(encoded == bin)
}
}
@@ -853,4 +861,85 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
assert(lightningMessageCodec.encode(ref).require.bytes == bin)
}
}
+
+ test("channel_reestablish backwards-compatibility with legacy splice TLVs") {
+ val channelId = randomBytes32()
+ val key = randomKey()
+ val point = randomKey().publicKey
+ val txId1 = randomTxId()
+ val txId2 = randomTxId()
+ val txId3 = randomTxId()
+
+ def reestablish(tlvs: ChannelReestablishTlv*): ChannelReestablish = {
+ ChannelReestablish(channelId, 1, 0, key, point, TlvStream(tlvs: _*))
+ }
+
+ // Legacy TLVs: tag 0 (experimental next_funding) + tag 1 (experimental your_last_funding_locked) + tag 3 (experimental my_current_funding_locked).
+ {
+ val msg = reestablish(
+ ChannelReestablishTlv.ExperimentalNextFundingTlv(txId1),
+ ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv.asExperimentalYourLastFundingLocked(txId2),
+ ChannelReestablishTlv.ExperimentalMyCurrentFundingLockedTlv(txId3),
+ )
+ assert(msg.nextFundingTxId_opt.contains(txId1))
+ assert(!msg.retransmitInteractiveTxCommitSig)
+ assert(msg.yourLastFundingLocked_opt.contains(txId2))
+ assert(msg.myCurrentFundingLocked_opt.contains(txId3))
+ assert(!msg.retransmitAnnSigs)
+ val encoded = lightningMessageCodec.encode(msg).require
+ assert(lightningMessageCodec.decode(encoded).require.value == msg)
+ }
+
+ // Official TLVs with retransmit flags set: tag 1 (official next_funding) + tag 5 (my_current_funding_locked).
+ {
+ val msg = reestablish(
+ ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv.asNextFunding(txId1, retransmitCommitSig = true),
+ ChannelReestablishTlv.MyCurrentFundingLockedTlv(txId2, retransmitAnnSigs = true),
+ )
+ assert(msg.nextFundingTxId_opt.contains(txId1))
+ assert(msg.retransmitInteractiveTxCommitSig)
+ assert(msg.yourLastFundingLocked_opt.isEmpty)
+ assert(msg.myCurrentFundingLocked_opt.contains(txId2))
+ assert(msg.retransmitAnnSigs)
+ val encoded = lightningMessageCodec.encode(msg).require
+ assert(lightningMessageCodec.decode(encoded).require.value == msg)
+ }
+
+ // Official TLVs with retransmit flags unset.
+ {
+ val msg = reestablish(
+ ChannelReestablishTlv.NextFundingOrExperimentalYourLastFundingLockedTlv.asNextFunding(txId1, retransmitCommitSig = false),
+ ChannelReestablishTlv.MyCurrentFundingLockedTlv(txId2, retransmitAnnSigs = false),
+ )
+ assert(msg.nextFundingTxId_opt.contains(txId1))
+ assert(!msg.retransmitInteractiveTxCommitSig)
+ assert(msg.yourLastFundingLocked_opt.isEmpty)
+ assert(msg.myCurrentFundingLocked_opt.contains(txId2))
+ assert(!msg.retransmitAnnSigs)
+ val encoded = lightningMessageCodec.encode(msg).require
+ assert(lightningMessageCodec.decode(encoded).require.value == msg)
+ }
+
+ // my_current_funding_locked priority: official tag 5 takes priority over legacy tag 3.
+ {
+ val msg = reestablish(
+ ChannelReestablishTlv.ExperimentalMyCurrentFundingLockedTlv(txId1),
+ ChannelReestablishTlv.MyCurrentFundingLockedTlv(txId2, retransmitAnnSigs = false),
+ )
+ assert(msg.myCurrentFundingLocked_opt.contains(txId2))
+ val encoded = lightningMessageCodec.encode(msg).require
+ assert(lightningMessageCodec.decode(encoded).require.value == msg)
+ }
+
+ // Empty TLV stream: all splice-related accessors return None/false.
+ {
+ val msg = ChannelReestablish(channelId, 1, 0, key, point)
+ assert(msg.nextFundingTxId_opt.isEmpty)
+ assert(!msg.retransmitInteractiveTxCommitSig)
+ assert(msg.yourLastFundingLocked_opt.isEmpty)
+ assert(msg.myCurrentFundingLocked_opt.isEmpty)
+ assert(!msg.retransmitAnnSigs)
+ }
+ }
+
}
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.