Skip to content

Commit e498115

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 deab192 commit e498115

File tree

5 files changed

+224
-241
lines changed

5 files changed

+224
-241
lines changed

docs/release-notes/eclair-vnext.md

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

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

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

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
@@ -91,6 +91,7 @@ case class InvalidRbfAttemptTooSoon (override val channelId: Byte
9191
case class InvalidSpliceTxAbortNotAcked (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid splice attempt: our previous tx_abort has not been acked")
9292
case class InvalidSpliceNotQuiescent (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid splice attempt: the channel is not quiescent")
9393
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(", ")})")
94+
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")
9495
case class InvalidRbfTxConfirmed (override val channelId: ByteVector32) extends ChannelException(channelId, "no need to rbf, transaction is already confirmed")
9596
case class InvalidRbfNonInitiator (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're not the initiator of this interactive-tx attempt")
9697
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
@@ -986,6 +986,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
986986
val previousTxs = d.commitments.active.filter(_.fundingTxIndex == d.commitments.latest.fundingTxIndex).map(_.fundingTxId)
987987
log.info("rejecting splice request: the previous splice has unconfirmed rbf attempts ({})", previousTxs.mkString(", "))
988988
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceWithUnconfirmedRbf(d.channelId, previousTxs).getMessage)
989+
} else if (d.commitments.latest.localFundingStatus.isInstanceOf[LocalFundingStatus.DualFundedUnconfirmedFundingTx]) {
990+
log.info("rejecting splice request: the previous funding transaction is unconfirmed ({})", d.commitments.latest.fundingTxId)
991+
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceWithUnconfirmedTx(d.channelId, d.commitments.latest.fundingTxId).getMessage)
989992
} else {
990993
val parentCommitment = d.commitments.latest.commitment
991994
val localFundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey

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
@@ -706,7 +706,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
706706
}
707707
case _ =>
708708
// This can happen if we received a tx_abort right before receiving the interactive-tx result.
709-
log.warning("ignoring interactive-tx result with rbfStatus={}", d.status.getClass.getSimpleName)
709+
log.warning("ignoring interactive-tx result with funding status={}", d.status.getClass.getSimpleName)
710710
stay()
711711
}
712712

0 commit comments

Comments
 (0)