Skip to content

Commit

Permalink
Disallow chains of unconfirmed splice transactions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast committed Aug 27, 2024
1 parent 2d350e5 commit cb02ea9
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 200 deletions.
3 changes: 3 additions & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ eclair-cli spliceout --channelId=<channel_id> --amountOut=<amount_satoshis> --sc

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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ case class InvalidRbfAttemptTooSoon (override val channelId: Byte
case class InvalidSpliceTxAbortNotAcked (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid splice attempt: our previous tx_abort has not been acked")
case class InvalidSpliceNotQuiescent (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid splice attempt: the channel is not quiescent")
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(", ")})")
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")
case class InvalidRbfTxConfirmed (override val channelId: ByteVector32) extends ChannelException(channelId, "no need to rbf, transaction is already confirmed")
case class InvalidRbfNonInitiator (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're not the initiator of this interactive-tx attempt")
case class InvalidRbfZeroConf (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're using zero-conf for this interactive-tx attempt")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
val previousTxs = d.commitments.active.filter(_.fundingTxIndex == d.commitments.latest.fundingTxIndex).map(_.fundingTxId)
log.info("rejecting splice request: the previous splice has unconfirmed rbf attempts ({})", previousTxs.mkString(", "))
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceWithUnconfirmedRbf(d.channelId, previousTxs).getMessage)
} else if (d.commitments.latest.localFundingStatus.isInstanceOf[LocalFundingStatus.DualFundedUnconfirmedFundingTx]) {
log.info("rejecting splice request: the previous funding transaction is unconfirmed ({})", d.commitments.latest.fundingTxId)
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceWithUnconfirmedTx(d.channelId, d.commitments.latest.fundingTxId).getMessage)
} else {
log.info(s"accepting splice with remote.in.amount=${msg.fundingContribution} remote.in.push=${msg.pushAmount}")
val parentCommitment = d.commitments.latest.commitment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
}
case _ =>
// This can happen if we received a tx_abort right before receiving the interactive-tx result.
log.warning("ignoring interactive-tx result with rbfStatus={}", d.status.getClass.getSimpleName)
log.warning("ignoring interactive-tx result with funding status={}", d.status.getClass.getSimpleName)
stay()
}

Expand Down
Loading

0 comments on commit cb02ea9

Please sign in to comment.