Skip to content

Commit 4959bed

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. 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. When using 0-conf, we reject `tx_init_rbf` and allow creating chains of unconfirmed splice transactions: using RBF with 0-conf can lead to one side stealing funds, which is why we prevent it. If our peer was buying liquidity but tries to cancel the purchase with an RBF attempt, we reject it: this prevents edge cases where the seller may end up adding liquidity to the channel without being paid in return.
1 parent 76c0a42 commit 4959bed

File tree

18 files changed

+1346
-511
lines changed

18 files changed

+1346
-511
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup
3838
- `channelstats` now takes optional parameters `--count` and `--skip` to control pagination. By default, it will return first 10 entries. (#2890)
3939
- `createinvoice` now takes an optional `--privateChannelIds` parameter that can be used to add routing hints through private channels. (#2909)
4040
- `nodes` allows filtering nodes that offer liquidity ads (#2848)
41+
- `rbfsplice` lets any channel participant RBF the current unconfirmed splice transaction (#2887)
4142

4243
### Miscellaneous improvements and bug fixes
4344

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

Lines changed: 29 additions & 20 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]]]]
@@ -228,17 +230,18 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
228230
}
229231

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

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

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

260269
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]]]] = {
@@ -575,9 +584,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
575584
case Left(channelId) => appKit.register ? Register.Forward(null, channelId, request)
576585
case Right(shortChannelId) => appKit.register ? Register.ForwardShortId(null, shortChannelId, request)
577586
}).map {
578-
case t: R@unchecked => t
579-
case t: Register.ForwardFailure[C]@unchecked => throw ChannelNotFound(Left(t.fwd.channelId))
580-
case t: Register.ForwardShortIdFailure[C]@unchecked => throw ChannelNotFound(Right(t.fwd.shortChannelId))
587+
case t: R @unchecked => t
588+
case t: Register.ForwardFailure[C] @unchecked => throw ChannelNotFound(Left(t.fwd.channelId))
589+
case t: Register.ForwardShortIdFailure[C] @unchecked => throw ChannelNotFound(Right(t.fwd.shortChannelId))
581590
}
582591

583592
private def sendToChannelTyped[C <: Command, R <: CommandResponse[C]](channel: ApiTypes.ChannelIdentifier, cmdBuilder: akka.actor.typed.ActorRef[Any] => C)(implicit timeout: Timeout): Future[R] =
@@ -588,9 +597,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
588597
case Right(shortChannelId) => Register.ForwardShortId(replyTo, shortChannelId, cmd)
589598
}
590599
}.map {
591-
case t: R@unchecked => t
592-
case t: Register.ForwardFailure[C]@unchecked => throw ChannelNotFound(Left(t.fwd.channelId))
593-
case t: Register.ForwardShortIdFailure[C]@unchecked => throw ChannelNotFound(Right(t.fwd.shortChannelId))
600+
case t: R @unchecked => t
601+
case t: Register.ForwardFailure[C] @unchecked => throw ChannelNotFound(Left(t.fwd.channelId))
602+
case t: Register.ForwardShortIdFailure[C] @unchecked => throw ChannelNotFound(Right(t.fwd.shortChannelId))
594603
}
595604

596605
/**

0 commit comments

Comments
 (0)