Skip to content

Commit e4e74e3

Browse files
committed
Remove support for splicing without quiescence
We initially supported splicing with a poor man's quiescence, where we allowed splice messages if the commitments were already quiescent. We've shipped support for quiescence since then, which means that new even nodes relying on experimental splicing should support quiescence. We can thus remove support for the non-quiescent version.
1 parent d087230 commit e4e74e3

File tree

6 files changed

+120
-273
lines changed

6 files changed

+120
-273
lines changed

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,6 @@ case class ChannelParams(channelId: ByteVector32,
133133
else Right(remoteScriptPubKey)
134134
}
135135

136-
/** If both peers support quiescence, we have to exchange stfu when splicing. */
137-
def useQuiescence: Boolean = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.Quiescence)
138-
139136
}
140137

141138
object ChannelParams {
@@ -824,7 +821,7 @@ case class Commitments(params: ChannelParams,
824821
def localIsQuiescent: Boolean = changes.localChanges.all.isEmpty
825822
def remoteIsQuiescent: Boolean = changes.remoteChanges.all.isEmpty
826823
// HTLCs and pending changes are the same for all active commitments, so we don't need to loop through all of them.
827-
def isQuiescent: Boolean = (params.useQuiescence || active.head.hasNoPendingHtlcs) && localIsQuiescent && remoteIsQuiescent
824+
def isQuiescent: Boolean = localIsQuiescent && remoteIsQuiescent
828825
def hasNoPendingHtlcsOrFeeUpdate: Boolean = active.head.hasNoPendingHtlcsOrFeeUpdate(changes)
829826
def hasPendingOrProposedHtlcs: Boolean = active.head.hasPendingOrProposedHtlcs(changes)
830827
def timedOutOutgoingHtlcs(currentHeight: BlockHeight): Set[UpdateAddHtlc] = active.head.timedOutOutgoingHtlcs(currentHeight)

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

Lines changed: 41 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -853,21 +853,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
853853
case Event(cmd: CMD_SPLICE, d: DATA_NORMAL) =>
854854
if (d.commitments.params.remoteParams.initFeatures.hasFeature(Features.Splicing)) {
855855
d.spliceStatus match {
856-
case SpliceStatus.NoSplice if d.commitments.params.useQuiescence =>
856+
case SpliceStatus.NoSplice =>
857857
startSingleTimer(QuiescenceTimeout.toString, QuiescenceTimeout(peer), nodeParams.channelConf.quiescenceTimeout)
858858
if (d.commitments.localIsQuiescent) {
859859
stay() using d.copy(spliceStatus = SpliceStatus.InitiatorQuiescent(cmd)) sending Stfu(d.channelId, initiator = true)
860860
} else {
861861
stay() using d.copy(spliceStatus = SpliceStatus.QuiescenceRequested(cmd))
862862
}
863-
case SpliceStatus.NoSplice if !d.commitments.params.useQuiescence =>
864-
initiateSplice(cmd, d) match {
865-
case Left(f) =>
866-
cmd.replyTo ! RES_FAILURE(cmd, f)
867-
stay()
868-
case Right(spliceInit) =>
869-
stay() using d.copy(spliceStatus = SpliceStatus.SpliceRequested(cmd, spliceInit)) sending spliceInit
870-
}
871863
case _ =>
872864
log.warning("cannot initiate splice, another one is already in progress")
873865
cmd.replyTo ! RES_FAILURE(cmd, InvalidSpliceAlreadyInProgress(d.channelId))
@@ -885,62 +877,53 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
885877
stay()
886878

887879
case Event(msg: Stfu, d: DATA_NORMAL) =>
888-
if (d.commitments.params.useQuiescence) {
889-
if (d.commitments.remoteIsQuiescent) {
890-
d.spliceStatus match {
891-
case SpliceStatus.NoSplice =>
892-
startSingleTimer(QuiescenceTimeout.toString, QuiescenceTimeout(peer), nodeParams.channelConf.quiescenceTimeout)
893-
if (d.commitments.localIsQuiescent) {
894-
stay() using d.copy(spliceStatus = SpliceStatus.NonInitiatorQuiescent) sending Stfu(d.channelId, initiator = false)
895-
} else {
896-
stay() using d.copy(spliceStatus = SpliceStatus.ReceivedStfu(msg))
897-
}
898-
case SpliceStatus.QuiescenceRequested(cmd) =>
899-
// We could keep track of our splice attempt and merge it with the remote splice instead of cancelling it.
900-
// But this is an edge case that should rarely occur, so it's probably not worth the additional complexity.
901-
log.warning("our peer initiated quiescence before us, cancelling our splice attempt")
902-
cmd.replyTo ! RES_FAILURE(cmd, ConcurrentRemoteSplice(d.channelId))
880+
if (d.commitments.remoteIsQuiescent) {
881+
d.spliceStatus match {
882+
case SpliceStatus.NoSplice =>
883+
startSingleTimer(QuiescenceTimeout.toString, QuiescenceTimeout(peer), nodeParams.channelConf.quiescenceTimeout)
884+
if (d.commitments.localIsQuiescent) {
885+
stay() using d.copy(spliceStatus = SpliceStatus.NonInitiatorQuiescent) sending Stfu(d.channelId, initiator = false)
886+
} else {
903887
stay() using d.copy(spliceStatus = SpliceStatus.ReceivedStfu(msg))
904-
case SpliceStatus.InitiatorQuiescent(cmd) =>
905-
// if both sides send stfu at the same time, the quiescence initiator is the channel opener
906-
if (!msg.initiator || d.commitments.params.localParams.isChannelOpener) {
907-
initiateSplice(cmd, d) match {
908-
case Left(f) =>
909-
cmd.replyTo ! RES_FAILURE(cmd, f)
910-
context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId))
911-
stay() using d.copy(spliceStatus = SpliceStatus.NoSplice) sending Warning(d.channelId, f.getMessage)
912-
case Right(spliceInit) =>
913-
stay() using d.copy(spliceStatus = SpliceStatus.SpliceRequested(cmd, spliceInit)) sending spliceInit
914-
}
915-
} else {
916-
log.warning("concurrent stfu received and our peer is the channel initiator, cancelling our splice attempt")
917-
cmd.replyTo ! RES_FAILURE(cmd, ConcurrentRemoteSplice(d.channelId))
918-
stay() using d.copy(spliceStatus = SpliceStatus.NonInitiatorQuiescent)
888+
}
889+
case SpliceStatus.QuiescenceRequested(cmd) =>
890+
// We could keep track of our splice attempt and merge it with the remote splice instead of cancelling it.
891+
// But this is an edge case that should rarely occur, so it's probably not worth the additional complexity.
892+
log.warning("our peer initiated quiescence before us, cancelling our splice attempt")
893+
cmd.replyTo ! RES_FAILURE(cmd, ConcurrentRemoteSplice(d.channelId))
894+
stay() using d.copy(spliceStatus = SpliceStatus.ReceivedStfu(msg))
895+
case SpliceStatus.InitiatorQuiescent(cmd) =>
896+
// if both sides send stfu at the same time, the quiescence initiator is the channel opener
897+
if (!msg.initiator || d.commitments.params.localParams.isChannelOpener) {
898+
initiateSplice(cmd, d) match {
899+
case Left(f) =>
900+
cmd.replyTo ! RES_FAILURE(cmd, f)
901+
context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId))
902+
stay() using d.copy(spliceStatus = SpliceStatus.NoSplice) sending Warning(d.channelId, f.getMessage)
903+
case Right(spliceInit) =>
904+
stay() using d.copy(spliceStatus = SpliceStatus.SpliceRequested(cmd, spliceInit)) sending spliceInit
919905
}
920-
case _ =>
921-
log.warning("ignoring duplicate stfu")
922-
stay()
923-
}
924-
} else {
925-
log.warning("our peer sent stfu but is not quiescent")
926-
// NB: we use a small delay to ensure we've sent our warning before disconnecting.
927-
context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId))
928-
stay() using d.copy(spliceStatus = SpliceStatus.NoSplice) sending Warning(d.channelId, InvalidSpliceNotQuiescent(d.channelId).getMessage)
906+
} else {
907+
log.warning("concurrent stfu received and our peer is the channel initiator, cancelling our splice attempt")
908+
cmd.replyTo ! RES_FAILURE(cmd, ConcurrentRemoteSplice(d.channelId))
909+
stay() using d.copy(spliceStatus = SpliceStatus.NonInitiatorQuiescent)
910+
}
911+
case _ =>
912+
log.warning("ignoring duplicate stfu")
913+
stay()
929914
}
930915
} else {
931-
log.warning("ignoring stfu because both peers do not advertise quiescence")
932-
stay()
916+
log.warning("our peer sent stfu but is not quiescent")
917+
// NB: we use a small delay to ensure we've sent our warning before disconnecting.
918+
context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId))
919+
stay() using d.copy(spliceStatus = SpliceStatus.NoSplice) sending Warning(d.channelId, InvalidSpliceNotQuiescent(d.channelId).getMessage)
933920
}
934921

935922
case Event(_: QuiescenceTimeout, d: DATA_NORMAL) => handleQuiescenceTimeout(d)
936923

937-
case Event(_: SpliceInit, d: DATA_NORMAL) if d.spliceStatus == SpliceStatus.NoSplice && d.commitments.params.useQuiescence =>
938-
log.info("rejecting splice attempt: quiescence not negotiated")
939-
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceNotQuiescent(d.channelId).getMessage)
940-
941924
case Event(msg: SpliceInit, d: DATA_NORMAL) =>
942925
d.spliceStatus match {
943-
case SpliceStatus.NoSplice | SpliceStatus.NonInitiatorQuiescent =>
926+
case SpliceStatus.NonInitiatorQuiescent =>
944927
if (!d.commitments.isQuiescent) {
945928
log.info("rejecting splice request: channel not quiescent")
946929
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceNotQuiescent(d.channelId).getMessage)
@@ -981,6 +964,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
981964
txBuilder ! InteractiveTxBuilder.Start(self)
982965
stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = None, sessionId, txBuilder, remoteCommitSig = None)) sending spliceAck
983966
}
967+
case SpliceStatus.NoSplice =>
968+
log.info("rejecting splice attempt: quiescence not negotiated")
969+
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceNotQuiescent(d.channelId).getMessage)
984970
case SpliceStatus.SpliceAborted =>
985971
log.info("rejecting splice attempt: our previous tx_abort was not acked")
986972
stay() sending Warning(d.channelId, InvalidSpliceTxAbortNotAcked(d.channelId).getMessage)

eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ object TestConstants {
102102
Wumbo -> Optional,
103103
PaymentMetadata -> Optional,
104104
RouteBlinding -> Optional,
105-
StaticRemoteKey -> Mandatory
105+
StaticRemoteKey -> Mandatory,
106+
Quiescence -> Optional,
107+
Splicing -> Optional,
106108
),
107109
unknown = Set(UnknownFeature(TestFeature.optional))
108110
),
@@ -274,7 +276,9 @@ object TestConstants {
274276
PaymentMetadata -> Optional,
275277
RouteBlinding -> Optional,
276278
StaticRemoteKey -> Mandatory,
277-
AnchorOutputsZeroFeeHtlcTx -> Optional
279+
AnchorOutputsZeroFeeHtlcTx -> Optional,
280+
Quiescence -> Optional,
281+
Splicing -> Optional,
278282
),
279283
pluginParams = Nil,
280284
overrideInitFeatures = Map.empty,

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ object ChannelStateTestsTags {
5151
val DisableWumbo = "disable_wumbo"
5252
/** If set, channels will use option_dual_fund. */
5353
val DualFunding = "dual_funding"
54-
/** If set, peers will support splicing. */
55-
val Splicing = "splicing"
5654
/** If set, channels will use option_static_remotekey. */
5755
val StaticRemoteKey = "static_remotekey"
5856
/** If set, channels will use option_anchor_outputs. */
@@ -91,8 +89,6 @@ object ChannelStateTestsTags {
9189
val RejectRbfAttempts = "reject_rbf_attempts"
9290
/** If set, the non-initiator will require a 1-block delay between RBF attempts. */
9391
val DelayRbfAttempts = "delay_rbf_attempts"
94-
/** If set, peers will support the quiesce protocol. */
95-
val Quiescence = "quiescence"
9692
/** If set, channels will adapt their max HTLC amount to the available balance */
9793
val AdaptMaxHtlcAmount = "adapt-max-htlc-amount"
9894
}
@@ -163,7 +159,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
163159
.modify(_.channelConf.balanceThresholds).setToIf(tags.contains(ChannelStateTestsTags.AdaptMaxHtlcAmount))(Seq(Channel.BalanceThreshold(1_000 sat, 0 sat), Channel.BalanceThreshold(5_000 sat, 1_000 sat), Channel.BalanceThreshold(10_000 sat, 5_000 sat)))
164160
val wallet = wallet_opt match {
165161
case Some(wallet) => wallet
166-
case None => if (tags.contains(ChannelStateTestsTags.DualFunding) || tags.contains(ChannelStateTestsTags.Splicing)) new SingleKeyOnChainWallet() else new DummyOnChainWallet()
162+
case None => if (tags.contains(ChannelStateTestsTags.DualFunding)) new SingleKeyOnChainWallet() else new DummyOnChainWallet()
167163
}
168164
val alice: TestFSMRef[ChannelState, ChannelData, Channel] = {
169165
implicit val system: ActorSystem = systemA
@@ -190,8 +186,6 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
190186
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional))
191187
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional))
192188
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional))
193-
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Splicing))(_.updated(Features.Splicing, FeatureSupport.Optional))
194-
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Quiescence))(_.updated(Features.Quiescence, FeatureSupport.Optional))
195189
.initFeatures()
196190
val bobInitFeatures = Bob.nodeParams.features
197191
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DisableWumbo))(_.removed(Features.Wumbo))
@@ -204,8 +198,6 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
204198
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional))
205199
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional))
206200
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.DualFunding, FeatureSupport.Optional))
207-
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Splicing))(_.updated(Features.Splicing, FeatureSupport.Optional))
208-
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Quiescence))(_.updated(Features.Quiescence, FeatureSupport.Optional))
209201
.initFeatures()
210202

211203
val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures, announceChannel = channelFlags.announceChannel)

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
4545
implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging
4646

4747
override def withFixture(test: OneArgTest): Outcome = {
48-
val tags = test.tags + ChannelStateTestsTags.DualFunding + ChannelStateTestsTags.Splicing + ChannelStateTestsTags.Quiescence
48+
val tags = test.tags + ChannelStateTestsTags.DualFunding
4949
val setup = init(tags = tags)
5050
import setup._
5151
reachNormal(setup, tags)

0 commit comments

Comments
 (0)