Skip to content

Commit 7d1c23e

Browse files
committed
Add support for RBF-ing splice transactions
If the latest splice transaction doesn't confirm, we allow exchanging `tx_init_rbf` and `tx_ack_rbf` to create another splice transaction to replace it. We use the same funding contribution as the previous splice. We disallow creating another splice transaction using `splice_init` if we have several RBF attempts for the latest splice: we cannot know which one of them will confirm and should be spent by the new splice. TODO: needs tests
1 parent 81dcf8f commit 7d1c23e

File tree

13 files changed

+435
-192
lines changed

13 files changed

+435
-192
lines changed

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

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningS
2626
import fr.acinq.eclair.io.Peer
2727
import fr.acinq.eclair.transactions.CommitmentSpec
2828
import fr.acinq.eclair.transactions.Transactions._
29-
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
29+
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxInitRbf, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
3030
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, TimestampMilli, UInt64}
3131
import scodec.bits.ByteVector
3232

@@ -193,36 +193,39 @@ sealed trait Command extends PossiblyHarmful
193193
sealed trait HasReplyToCommand extends Command { def replyTo: ActorRef }
194194
sealed trait HasOptionalReplyToCommand extends Command { def replyTo_opt: Option[ActorRef] }
195195

196-
sealed trait ForbiddenCommandDuringSplice extends Command
197-
sealed trait ForbiddenCommandDuringQuiescence extends Command
196+
sealed trait ForbiddenCommandDuringQuiescenceNegotiation extends Command
197+
sealed trait ForbiddenCommandWhenQuiescent extends Command
198198

199-
final case class CMD_ADD_HTLC(replyTo: ActorRef, amount: MilliSatoshi, paymentHash: ByteVector32, cltvExpiry: CltvExpiry, onion: OnionRoutingPacket, nextBlindingKey_opt: Option[PublicKey], confidence: Double, origin: Origin.Hot, commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence
200-
sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence { def id: Long }
199+
final case class CMD_ADD_HTLC(replyTo: ActorRef, amount: MilliSatoshi, paymentHash: ByteVector32, cltvExpiry: CltvExpiry, onion: OnionRoutingPacket, nextBlindingKey_opt: Option[PublicKey], confidence: Double, origin: Origin.Hot, commit: Boolean = false) extends HasReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent
200+
sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent { def id: Long }
201201
final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
202202
final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
203203
final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
204-
final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence
205-
final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringSplice
204+
final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent
205+
final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandWhenQuiescent
206206

207207
final case class ClosingFees(preferred: Satoshi, min: Satoshi, max: Satoshi)
208208
final case class ClosingFeerates(preferred: FeeratePerKw, min: FeeratePerKw, max: FeeratePerKw) {
209209
def computeFees(closingTxWeight: Int): ClosingFees = ClosingFees(weight2fee(preferred, closingTxWeight), weight2fee(min, closingTxWeight), weight2fee(max, closingTxWeight))
210210
}
211211

212212
sealed trait CloseCommand extends HasReplyToCommand
213-
final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector], feerates: Option[ClosingFeerates]) extends CloseCommand with ForbiddenCommandDuringSplice with ForbiddenCommandDuringQuiescence
213+
final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector], feerates: Option[ClosingFeerates]) extends CloseCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent
214214
final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand
215215
final case class CMD_BUMP_FORCE_CLOSE_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FORCE_CLOSE_FEE]], confirmationTarget: ConfirmationTarget) extends Command
216216

217-
final case class CMD_BUMP_FUNDING_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FUNDING_FEE]], targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime: Long) extends Command
217+
sealed trait ChannelFundingCommand extends Command {
218+
def replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]]
219+
}
218220
case class SpliceIn(additionalLocalFunding: Satoshi, pushAmount: MilliSatoshi = 0 msat)
219221
case class SpliceOut(amount: Satoshi, scriptPubKey: ByteVector)
220-
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_SPLICE]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]) extends Command {
222+
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]) extends ChannelFundingCommand {
221223
require(spliceIn_opt.isDefined || spliceOut_opt.isDefined, "there must be a splice-in or a splice-out")
222224
val additionalLocalFunding: Satoshi = spliceIn_opt.map(_.additionalLocalFunding).getOrElse(0 sat)
223225
val pushAmount: MilliSatoshi = spliceIn_opt.map(_.pushAmount).getOrElse(0 msat)
224226
val spliceOutputs: List[TxOut] = spliceOut_opt.toList.map(s => TxOut(s.amount, s.scriptPubKey))
225227
}
228+
final case class CMD_BUMP_FUNDING_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime: Long) extends ChannelFundingCommand
226229
final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long) extends HasReplyToCommand
227230
final case class CMD_GET_CHANNEL_STATE(replyTo: ActorRef) extends HasReplyToCommand
228231
final case class CMD_GET_CHANNEL_DATA(replyTo: ActorRef) extends HasReplyToCommand
@@ -456,42 +459,61 @@ object RemoteFundingStatus {
456459
case object Locked extends RemoteFundingStatus
457460
}
458461

459-
sealed trait RbfStatus
460-
object RbfStatus {
461-
case object NoRbf extends RbfStatus
462-
case class RbfRequested(cmd: CMD_BUMP_FUNDING_FEE) extends RbfStatus
463-
case class RbfInProgress(cmd_opt: Option[CMD_BUMP_FUNDING_FEE], rbf: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends RbfStatus
464-
case class RbfWaitingForSigs(signingSession: InteractiveTxSigningSession.WaitingForSigs) extends RbfStatus
465-
case object RbfAborted extends RbfStatus
462+
sealed trait DualFundingStatus
463+
object DualFundingStatus {
464+
/** We're waiting for one of the funding transactions to confirm. */
465+
case object WaitingForConfirmations extends DualFundingStatus
466+
/** We told our peer we want to RBF the funding transaction. */
467+
case class RbfRequested(cmd: CMD_BUMP_FUNDING_FEE) extends DualFundingStatus
468+
/** We both agreed to RBF and are building the new funding transaction. */
469+
case class RbfInProgress(cmd_opt: Option[CMD_BUMP_FUNDING_FEE], rbf: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends DualFundingStatus
470+
/** A new funding transaction has been negotiated, we're exchanging signatures. */
471+
case class RbfWaitingForSigs(signingSession: InteractiveTxSigningSession.WaitingForSigs) extends DualFundingStatus
472+
/** The RBF attempt was aborted by us, we're waiting for our peer to ack. */
473+
case object RbfAborted extends DualFundingStatus
466474
}
467475

468-
sealed trait SpliceStatus
469476
/** We're waiting for the channel to be quiescent. */
470-
sealed trait QuiescenceNegotiation extends SpliceStatus
477+
sealed trait QuiescenceNegotiation
471478
object QuiescenceNegotiation {
472479
sealed trait Initiator extends QuiescenceNegotiation
480+
object Initiator {
481+
/** We stop sending new updates and wait for our updates to be added to the local and remote commitments. */
482+
case object QuiescenceRequested extends Initiator
483+
/** Our updates have been added to the local and remote commitments, we wait for our peer to do the same. */
484+
case class SentStfu(stfu: Stfu) extends Initiator
485+
}
486+
473487
sealed trait NonInitiator extends QuiescenceNegotiation
488+
object NonInitiator {
489+
/** Our peer has asked us to stop sending new updates and wait for our updates to be added to the local and remote commitments. */
490+
case class ReceivedStfu(stfu: Stfu) extends NonInitiator
491+
}
492+
}
493+
494+
sealed trait SpliceStatus {
495+
def isNegotiatingQuiescence: Boolean = this.isInstanceOf[SpliceStatus.NegotiatingQuiescence]
496+
def isQuiescent: Boolean = this match {
497+
case SpliceStatus.NoSplice | _: SpliceStatus.NegotiatingQuiescence => false
498+
case _ => true
499+
}
474500
}
475-
/** The channel is quiescent and a splice attempt was initiated. */
476-
sealed trait QuiescentSpliceStatus extends SpliceStatus
477501
object SpliceStatus {
478502
case object NoSplice extends SpliceStatus
479-
/** We stop sending new updates and wait for our updates to be added to the local and remote commitments. */
480-
case class QuiescenceRequested(splice: CMD_SPLICE) extends QuiescenceNegotiation.Initiator
481-
/** Our updates have been added to the local and remote commitments, we wait for our peer to do the same. */
482-
case class InitiatorQuiescent(splice: CMD_SPLICE) extends QuiescenceNegotiation.Initiator
483-
/** Our peer has asked us to stop sending new updates and wait for our updates to be added to the local and remote commitments. */
484-
case class ReceivedStfu(stfu: Stfu) extends QuiescenceNegotiation.NonInitiator
485-
/** Our updates have been added to the local and remote commitments, we wait for our peer to use the now quiescent channel. */
486-
case object NonInitiatorQuiescent extends QuiescentSpliceStatus
503+
/** We're trying to quiesce the channel in order to negotiate a splice. */
504+
case class NegotiatingQuiescence(cmd_opt: Option[ChannelFundingCommand], status: QuiescenceNegotiation) extends SpliceStatus
505+
/** The channel is quiescent, we wait for our peer to send splice_init or tx_init_rbf. */
506+
case object NonInitiatorQuiescent extends SpliceStatus
487507
/** We told our peer we want to splice funds in the channel. */
488-
case class SpliceRequested(cmd: CMD_SPLICE, init: SpliceInit) extends QuiescentSpliceStatus
489-
/** We both agreed to splice and are building the splice transaction. */
490-
case class SpliceInProgress(cmd_opt: Option[CMD_SPLICE], sessionId: ByteVector32, splice: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends QuiescentSpliceStatus
508+
case class SpliceRequested(cmd: CMD_SPLICE, init: SpliceInit) extends SpliceStatus
509+
/** We told our peer we want to RBF the latest splice transaction. */
510+
case class RbfRequested(cmd: CMD_BUMP_FUNDING_FEE, rbf: TxInitRbf) extends SpliceStatus
511+
/** We both agreed to splice/rbf and are building the corresponding transaction. */
512+
case class SpliceInProgress(cmd_opt: Option[ChannelFundingCommand], sessionId: ByteVector32, splice: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends SpliceStatus
491513
/** The splice transaction has been negotiated, we're exchanging signatures. */
492-
case class SpliceWaitingForSigs(signingSession: InteractiveTxSigningSession.WaitingForSigs) extends QuiescentSpliceStatus
514+
case class SpliceWaitingForSigs(signingSession: InteractiveTxSigningSession.WaitingForSigs) extends SpliceStatus
493515
/** The splice attempt was aborted by us, we're waiting for our peer to ack. */
494-
case object SpliceAborted extends QuiescentSpliceStatus
516+
case object SpliceAborted extends SpliceStatus
495517
}
496518

497519
sealed trait ChannelData extends PossiblyHarmful {
@@ -585,7 +607,7 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments,
585607
remotePushAmount: MilliSatoshi,
586608
waitingSince: BlockHeight, // how long have we been waiting for a funding tx to confirm
587609
lastChecked: BlockHeight, // last time we checked if the channel was double-spent
588-
rbfStatus: RbfStatus,
610+
status: DualFundingStatus,
589611
deferred: Option[ChannelReady]) extends ChannelDataWithCommitments {
590612
def allFundingTxs: Seq[DualFundedUnconfirmedFundingTx] = commitments.active.map(_.localFundingStatus).collect { case fundingTx: DualFundedUnconfirmedFundingTx => fundingTx }
591613
def latestFundingTx: DualFundedUnconfirmedFundingTx = commitments.latest.localFundingStatus.asInstanceOf[DualFundedUnconfirmedFundingTx]
@@ -600,7 +622,10 @@ final case class DATA_NORMAL(commitments: Commitments,
600622
localShutdown: Option[Shutdown],
601623
remoteShutdown: Option[Shutdown],
602624
closingFeerates: Option[ClosingFeerates],
603-
spliceStatus: SpliceStatus) extends ChannelDataWithCommitments
625+
spliceStatus: SpliceStatus) extends ChannelDataWithCommitments {
626+
val isNegotiatingQuiescence: Boolean = spliceStatus.isNegotiatingQuiescence
627+
val isQuiescent: Boolean = spliceStatus.isQuiescent
628+
}
604629
final case class DATA_SHUTDOWN(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]) extends ChannelDataWithCommitments
605630
final case class DATA_NEGOTIATING(commitments: Commitments,
606631
localShutdown: Shutdown, remoteShutdown: Shutdown,

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
@@ -85,6 +85,7 @@ case class InvalidRbfAttemptsExhausted (override val channelId: Byte
8585
case class InvalidRbfAttemptTooSoon (override val channelId: ByteVector32, previousAttempt: BlockHeight, nextAttempt: BlockHeight) extends ChannelException(channelId, s"invalid rbf attempt: last attempt made at block=$previousAttempt, next attempt available after block=$nextAttempt")
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")
88+
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(", ")})")
8889
case class InvalidRbfTxConfirmed (override val channelId: ByteVector32) extends ChannelException(channelId, "no need to rbf, transaction is already confirmed")
8990
case class InvalidRbfNonInitiator (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're not the initiator of this interactive-tx attempt")
9091
case class InvalidRbfZeroConf (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're using zero-conf for this interactive-tx attempt")

0 commit comments

Comments
 (0)