Skip to content

Commit fa51c31

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 e4e74e3 commit fa51c31

File tree

17 files changed

+802
-214
lines changed

17 files changed

+802
-214
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup
2626

2727
### API changes
2828

29-
- `channelstats` now takes optional parameters `--count` and `--skip` to control pagination. By default, it will return first 10 entries. (#2890)
29+
- `channelstats` now accept `--count` and `--skip` parameters to limit the number of retrieved items (#2890)
30+
- `rbfsplice` lets any channel participant RBF the current unconfirmed splice transaction (#2887)
3031

3132
### Miscellaneous improvements and bug fixes
3233

eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ trait Eclair {
9494

9595
def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
9696

97+
def rbfSplice(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]]
98+
9799
def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]]
98100

99101
def forceClose(channels: List[ApiTypes.ChannelIdentifier])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_FORCECLOSE]]]]
@@ -227,16 +229,18 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
227229
}
228230

229231
override def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] = {
230-
sendToChannelTyped(channel = Left(channelId),
231-
cmdBuilder = CMD_BUMP_FUNDING_FEE(_, targetFeerate, fundingFeeBudget, lockTime_opt.getOrElse(appKit.nodeParams.currentBlockHeight.toLong)))
232+
sendToChannelTyped(
233+
channel = Left(channelId),
234+
cmdBuilder = CMD_BUMP_FUNDING_FEE(_, targetFeerate, fundingFeeBudget, lockTime_opt.getOrElse(appKit.nodeParams.currentBlockHeight.toLong))
235+
)
232236
}
233237

234238
override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
235-
sendToChannelTyped(channel = Left(channelId),
236-
cmdBuilder = CMD_SPLICE(_,
237-
spliceIn_opt = Some(SpliceIn(additionalLocalFunding = amountIn, pushAmount = pushAmount_opt.getOrElse(0.msat))),
238-
spliceOut_opt = None
239-
))
239+
val spliceIn = SpliceIn(additionalLocalFunding = amountIn, pushAmount = pushAmount_opt.getOrElse(0.msat))
240+
sendToChannelTyped(
241+
channel = Left(channelId),
242+
cmdBuilder = CMD_SPLICE(_, spliceIn_opt = Some(spliceIn), spliceOut_opt = None)
243+
)
240244
}
241245

242246
override def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
@@ -247,11 +251,18 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
247251
case Right(script) => Script.write(script)
248252
}
249253
}
250-
sendToChannelTyped(channel = Left(channelId),
251-
cmdBuilder = CMD_SPLICE(_,
252-
spliceIn_opt = None,
253-
spliceOut_opt = Some(SpliceOut(amount = amountOut, scriptPubKey = script))
254-
))
254+
val spliceOut = SpliceOut(amount = amountOut, scriptPubKey = script)
255+
sendToChannelTyped(
256+
channel = Left(channelId),
257+
cmdBuilder = CMD_SPLICE(_, spliceIn_opt = None, spliceOut_opt = Some(spliceOut))
258+
)
259+
}
260+
261+
override def rbfSplice(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] = {
262+
sendToChannelTyped(
263+
channel = Left(channelId),
264+
cmdBuilder = CMD_BUMP_FUNDING_FEE(_, targetFeerate, fundingFeeBudget, lockTime_opt.getOrElse(appKit.nodeParams.currentBlockHeight.toLong))
265+
)
255266
}
256267

257268
override def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] = {
@@ -558,9 +569,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
558569
case Left(channelId) => appKit.register ? Register.Forward(null, channelId, request)
559570
case Right(shortChannelId) => appKit.register ? Register.ForwardShortId(null, shortChannelId, request)
560571
}).map {
561-
case t: R@unchecked => t
562-
case t: Register.ForwardFailure[C]@unchecked => throw ChannelNotFound(Left(t.fwd.channelId))
563-
case t: Register.ForwardShortIdFailure[C]@unchecked => throw ChannelNotFound(Right(t.fwd.shortChannelId))
572+
case t: R @unchecked => t
573+
case t: Register.ForwardFailure[C] @unchecked => throw ChannelNotFound(Left(t.fwd.channelId))
574+
case t: Register.ForwardShortIdFailure[C] @unchecked => throw ChannelNotFound(Right(t.fwd.shortChannelId))
564575
}
565576

566577
private def sendToChannelTyped[C <: Command, R <: CommandResponse[C]](channel: ApiTypes.ChannelIdentifier, cmdBuilder: akka.actor.typed.ActorRef[Any] => C)(implicit timeout: Timeout): Future[R] =
@@ -571,9 +582,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
571582
case Right(shortChannelId) => Register.ForwardShortId(replyTo, shortChannelId, cmd)
572583
}
573584
}.map {
574-
case t: R@unchecked => t
575-
case t: Register.ForwardFailure[C]@unchecked => throw ChannelNotFound(Left(t.fwd.channelId))
576-
case t: Register.ForwardShortIdFailure[C]@unchecked => throw ChannelNotFound(Right(t.fwd.shortChannelId))
585+
case t: R @unchecked => t
586+
case t: Register.ForwardFailure[C] @unchecked => throw ChannelNotFound(Left(t.fwd.channelId))
587+
case t: Register.ForwardShortIdFailure[C] @unchecked => throw ChannelNotFound(Right(t.fwd.shortChannelId))
577588
}
578589

579590
/**

0 commit comments

Comments
 (0)