Skip to content

Commit cb02ea9

Browse files
committed
Disallow chains of unconfirmed splice transactions
When 0-conf isn't used, we reject `splice_init` while the previous splice transaction hasn't confirmed. Our peer should either use RBF instead of creating a new splice, or they should wait for our node to receive the block that confirmed the previous transaction. This protects against chains of unconfirmed transactions.
1 parent 2d350e5 commit cb02ea9

File tree

5 files changed

+224
-200
lines changed

5 files changed

+224
-200
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ eclair-cli spliceout --channelId=<channel_id> --amountOut=<amount_satoshis> --sc
3131

3232
That operation can also be RBF-ed with the `rbfsplice` API to speed up confirmation if necessary.
3333

34+
Note that when 0-conf is used for the channel, it is not possible to RBF splice transactions.
35+
Node operators should instead create a new splice transaction (with `splicein` or `spliceout`) to CPFP the previous transaction.
36+
3437
Note that eclair had already introduced support for a splicing prototype in v0.9.0, which helped improve the BOLT proposal.
3538
We're removing support for the previous splicing prototype feature: users that depended on this protocol must upgrade to create official splice transactions.
3639

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ case class InvalidRbfAttemptTooSoon (override val channelId: Byte
8686
case class InvalidSpliceTxAbortNotAcked (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid splice attempt: our previous tx_abort has not been acked")
8787
case class InvalidSpliceNotQuiescent (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid splice attempt: the channel is not quiescent")
8888
case class InvalidSpliceWithUnconfirmedRbf (override val channelId: ByteVector32, previousTxs: Seq[TxId]) extends ChannelException(channelId, s"invalid splice attempt: the previous splice was rbf-ed and is still unconfirmed (txIds=${previousTxs.mkString(", ")})")
89+
case class InvalidSpliceWithUnconfirmedTx (override val channelId: ByteVector32, fundingTx: TxId) extends ChannelException(channelId, s"invalid splice attempt: the current funding transaction is still unconfirmed (txId=$fundingTx), you should use tx_init_rbf instead")
8990
case class InvalidRbfTxConfirmed (override val channelId: ByteVector32) extends ChannelException(channelId, "no need to rbf, transaction is already confirmed")
9091
case class InvalidRbfNonInitiator (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're not the initiator of this interactive-tx attempt")
9192
case class InvalidRbfZeroConf (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're using zero-conf for this interactive-tx attempt")

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
985985
val previousTxs = d.commitments.active.filter(_.fundingTxIndex == d.commitments.latest.fundingTxIndex).map(_.fundingTxId)
986986
log.info("rejecting splice request: the previous splice has unconfirmed rbf attempts ({})", previousTxs.mkString(", "))
987987
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceWithUnconfirmedRbf(d.channelId, previousTxs).getMessage)
988+
} else if (d.commitments.latest.localFundingStatus.isInstanceOf[LocalFundingStatus.DualFundedUnconfirmedFundingTx]) {
989+
log.info("rejecting splice request: the previous funding transaction is unconfirmed ({})", d.commitments.latest.fundingTxId)
990+
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceWithUnconfirmedTx(d.channelId, d.commitments.latest.fundingTxId).getMessage)
988991
} else {
989992
log.info(s"accepting splice with remote.in.amount=${msg.fundingContribution} remote.in.push=${msg.pushAmount}")
990993
val parentCommitment = d.commitments.latest.commitment

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
675675
}
676676
case _ =>
677677
// This can happen if we received a tx_abort right before receiving the interactive-tx result.
678-
log.warning("ignoring interactive-tx result with rbfStatus={}", d.status.getClass.getSimpleName)
678+
log.warning("ignoring interactive-tx result with funding status={}", d.status.getClass.getSimpleName)
679679
stay()
680680
}
681681

0 commit comments

Comments
 (0)