Skip to content

Commit 48f98f5

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. Disallow chains of unconfirmed splice transactions 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.
1 parent eb9227a commit 48f98f5

File tree

17 files changed

+1075
-456
lines changed

17 files changed

+1075
-456
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,39 @@ Every node advertizes the rates at which they sell their liquidity, and buyers c
1313
The liquidity ads specification is still under review and will likely change.
1414
This feature isn't meant to be used on mainnet yet and is thus disabled by default.
1515

16+
### Channel Splicing
17+
18+
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.
19+
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.
20+
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.
21+
22+
The size of an existing channel can be increased with the `splicein` API:
23+
24+
```sh
25+
eclair-cli splicein --channelId=<channel_id> --amountIn=<amount_satoshis>
26+
```
27+
28+
Once that transaction confirms, the additional liquidity can be used to send outgoing payments.
29+
If the transaction doesn't confirm, the node operator can speed up confirmation with the `rbfsplice` API:
30+
31+
```sh
32+
eclair-cli rbfsplice --channelId=<channel_id> --targetFeerateSatByte=<feerate_satoshis_per_byte> --fundingFeeBudgetSatoshis=<maximum_on_chain_fee_satoshis>
33+
```
34+
35+
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:
36+
37+
```sh
38+
eclair-cli spliceout --channelId=<channel_id> --amountOut=<amount_satoshis> --scriptPubKey=<on_chain_address>
39+
```
40+
41+
That operation can also be RBF-ed with the `rbfsplice` API to speed up confirmation if necessary.
42+
43+
Note that when 0-conf is used for the channel, it is not possible to RBF splice transactions.
44+
Node operators should instead create a new splice transaction (with `splicein` or `spliceout`) to CPFP the previous transaction.
45+
46+
Note that eclair had already introduced support for a splicing prototype in v0.9.0, which helped improve the BOLT proposal.
47+
We're removing support for the previous splicing prototype feature: users that depended on this protocol must upgrade to create official splice transactions.
48+
1649
### Update minimal version of Bitcoin Core
1750

1851
With this release, eclair requires using Bitcoin Core 27.1.
@@ -38,6 +71,7 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup
3871
- `channelstats` now takes optional parameters `--count` and `--skip` to control pagination. By default, it will return first 10 entries. (#2890)
3972
- `createinvoice` now takes an optional `--privateChannelIds` parameter that can be used to add routing hints through private channels. (#2909)
4073
- `nodes` allows filtering nodes that offer liquidity ads (#2848)
74+
- `rbfsplice` lets any channel participant RBF the current unconfirmed splice transaction (#2887)
4175

4276
### Miscellaneous improvements and bug fixes
4377

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)