Skip to content

Commit 2d350e5

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.
1 parent 64b4879 commit 2d350e5

File tree

17 files changed

+832
-214
lines changed

17 files changed

+832
-214
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@
44

55
## Major changes
66

7+
### Channel Splicing
8+
9+
With this release, we add support for the final version of [splicing](https://github.com/lightning/bolts/pull/1160) that was recently added to the BOLTs.
10+
Splicing allows node operators to change the size of their existing channels, which makes it easier and more efficient to allocate liquidity where it is most needed.
11+
Most node operators can now have a single channel with each of their peer, which costs less on-chain fees and resources, and makes path-finding easier.
12+
13+
The size of an existing channel can be increased with the `splicein` API:
14+
15+
```sh
16+
eclair-cli splicein --channelId=<channel_id> --amountIn=<amount_satoshis>
17+
```
18+
19+
Once that transaction confirms, the additional liquidity can be used to send outgoing payments.
20+
If the transaction doesn't confirm, the node operator can speed up confirmation with the `rbfsplice` API:
21+
22+
```sh
23+
eclair-cli rbfsplice --channelId=<channel_id> --targetFeerateSatByte=<feerate_satoshis_per_byte> --fundingFeeBudgetSatoshis=<maximum_on_chain_fee_satoshis>
24+
```
25+
26+
If the node operator wants to reduce the size of a channel, or send some of the channel funds to an on-chain address, they can use the `spliceout` API:
27+
28+
```sh
29+
eclair-cli spliceout --channelId=<channel_id> --amountOut=<amount_satoshis> --scriptPubKey=<on_chain_address>
30+
```
31+
32+
That operation can also be RBF-ed with the `rbfsplice` API to speed up confirmation if necessary.
33+
34+
Note that eclair had already introduced support for a splicing prototype in v0.9.0, which helped improve the BOLT proposal.
35+
We're removing support for the previous splicing prototype feature: users that depended on this protocol must upgrade to create official splice transactions.
36+
737
### Update minimal version of Bitcoin Core
838

939
With this release, eclair requires using Bitcoin Core 27.1.
@@ -26,7 +56,8 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup
2656

2757
### API changes
2858

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

3162
### Miscellaneous improvements and bug fixes
3263

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)