Skip to content

Use final spec values for splicing #2887

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,39 @@

<insert changes>

### Channel Splicing

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.
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.
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.

The size of an existing channel can be increased with the `splicein` API:

```sh
eclair-cli splicein --channelId=<channel_id> --amountIn=<amount_satoshis>
```

Once that transaction confirms, the additional liquidity can be used to send outgoing payments.
If the transaction doesn't confirm, the node operator can speed up confirmation with the `rbfsplice` API:

```sh
eclair-cli rbfsplice --channelId=<channel_id> --targetFeerateSatByte=<feerate_satoshis_per_byte> --fundingFeeBudgetSatoshis=<maximum_on_chain_fee_satoshis>
```

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:

```sh
eclair-cli spliceout --channelId=<channel_id> --amountOut=<amount_satoshis> --scriptPubKey=<on_chain_address>
```

That operation can also be RBF-ed with the `rbfsplice` API to speed up confirmation if necessary.

Note that when 0-conf is used for the channel, it is not possible to RBF splice transactions.
Node operators should instead create a new splice transaction (with `splicein` or `spliceout`) to CPFP the previous transaction.

Note that eclair had already introduced support for a splicing prototype in v0.9.0, which helped improve the BOLT proposal.
We're removing support for the previous splicing prototype feature: users that depended on this protocol must upgrade to create official splice transactions.

### Package relay

With Bitcoin Core 28.1, eclair starts relying on the `submitpackage` RPC during channel force-close.
Expand Down
1 change: 1 addition & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ eclair {
option_zeroconf = disabled
keysend = disabled
option_simple_close=optional
option_splice = optional
trampoline_payment_prototype = disabled
async_payment_prototype = disabled
on_the_fly_funding = disabled
Expand Down
17 changes: 7 additions & 10 deletions eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,7 @@ object Features {
val mandatory = 28
}

// TODO: this should also extend NodeFeature once the spec is finalized
case object Quiescence extends Feature with InitFeature {
case object Quiescence extends Feature with InitFeature with NodeFeature {
val rfcName = "option_quiesce"
val mandatory = 34
}
Expand Down Expand Up @@ -310,6 +309,11 @@ object Features {
val mandatory = 60
}

case object Splicing extends Feature with InitFeature with NodeFeature {
val rfcName = "option_splice"
val mandatory = 62
}

/** This feature bit indicates that the node is a mobile wallet that can be woken up via push notifications. */
case object WakeUpNotificationClient extends Feature with InitFeature {
val rfcName = "wake_up_notification_client"
Expand All @@ -333,12 +337,6 @@ object Features {
val mandatory = 152
}

// TODO: @pm47 custom splices implementation for phoenix, to be replaced once splices is spec-ed (currently reserved here: https://github.com/lightning/bolts/issues/605)
case object SplicePrototype extends Feature with InitFeature {
val rfcName = "splice_prototype"
val mandatory = 154
}

/**
* Activate this feature to provide on-the-fly funding to remote nodes, as specified in bLIP 36: https://github.com/lightning/blips/blob/master/blip-0036.md.
* TODO: add NodeFeature once bLIP is merged.
Expand Down Expand Up @@ -381,10 +379,10 @@ object Features {
ZeroConf,
KeySend,
SimpleClose,
Splicing,
WakeUpNotificationClient,
TrampolinePaymentPrototype,
AsyncPaymentPrototype,
SplicePrototype,
OnTheFlyFunding,
FundingFeeCredit
)
Expand All @@ -401,7 +399,6 @@ object Features {
KeySend -> (VariableLengthOnion :: Nil),
SimpleClose -> (ShutdownAnySegwit :: Nil),
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
OnTheFlyFunding -> (SplicePrototype :: Nil),
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,8 @@ case class Commitment(fundingTxIndex: Long,
Metrics.recordHtlcsInFlight(spec, remoteCommit.spec)

val tlvs = Set(
if (batchSize > 1) Some(CommitSigTlv.BatchTlv(batchSize)) else None
if (batchSize > 1) Some(CommitSigTlv.FundingTx(fundingTxId)) else None,
if (batchSize > 1) Some(CommitSigTlv.ExperimentalBatchTlv(batchSize)) else None,
).flatten[CommitSigTlv]
val commitSig = params.commitmentFormat match {
case _: SegwitV0CommitmentFormat =>
Expand Down Expand Up @@ -993,7 +994,7 @@ case class Commitments(params: ChannelParams,
}
}

def sendCommit(channelKeys: ChannelKeys)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, Seq[CommitSig])] = {
def sendCommit(channelKeys: ChannelKeys)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, CommitSigs)] = {
remoteNextCommitInfo match {
case Right(_) if !changes.localHasChanges => Left(CannotSignWithoutChanges(channelId))
case Right(remoteNextPerCommitmentPoint) =>
Expand All @@ -1007,21 +1008,26 @@ case class Commitments(params: ChannelParams,
active = active1,
remoteNextCommitInfo = Left(WaitForRev(localCommitIndex))
)
Right(commitments1, sigs)
Right(commitments1, CommitSigs(sigs))
case Left(_) => Left(CannotSignBeforeRevocation(channelId))
}
}

def receiveCommit(commits: Seq[CommitSig], channelKeys: ChannelKeys)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, RevokeAndAck)] = {
def receiveCommit(commitSigs: CommitSigs, channelKeys: ChannelKeys)(implicit log: LoggingAdapter): Either[ChannelException, (Commitments, RevokeAndAck)] = {
// We may receive more commit_sig than the number of active commitments, because there can be a race where we send
// splice_locked while our peer is sending us a batch of commit_sig. When that happens, we simply need to discard
// the commit_sig that belong to commitments we deactivated.
if (commits.size < active.size) {
return Left(CommitSigCountMismatch(channelId, active.size, commits.size))
val sigs = commitSigs match {
case batch: CommitSigBatch if batch.batchSize < active.size => return Left(CommitSigCountMismatch(channelId, active.size, batch.batchSize))
case batch: CommitSigBatch => batch.messages
case _: CommitSig if active.size > 1 => return Left(CommitSigCountMismatch(channelId, active.size, 1))
case commitSig: CommitSig => Seq(commitSig)
}
val commitKeys = LocalCommitmentKeys(params, channelKeys, localCommitIndex + 1)
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
val active1 = active.zip(commits).map { case (commitment, commit) =>
val active1 = active.zipWithIndex.map { case (commitment, idx) =>
// If the funding_txid isn't provided, we assume that signatures are sent in order (most recent first).
// This matches the behavior of peers who only support the experimental version of splicing.
val commit = sigs.find(_.fundingTxId_opt.contains(commitment.fundingTxId)).getOrElse(sigs(idx))
commitment.receiveCommit(params, channelKeys, commitKeys, changes, commit) match {
case Left(f) => return Left(f)
case Right(commitment1) => commitment1
Expand Down Expand Up @@ -1156,7 +1162,7 @@ case class Commitments(params: ChannelParams,
case ChannelSpendSignature.IndividualSignature(latestRemoteSig) => latestRemoteSig == commitSig.signature
case ChannelSpendSignature.PartialSignatureWithNonce(_, _) => ???
}
params.channelFeatures.hasFeature(Features.DualFunding) && commitSig.batchSize == 1 && isLatestSig
params.channelFeatures.hasFeature(Features.DualFunding) && isLatestSig
}

def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,14 +478,14 @@ object Helpers {
// we just sent a new commit_sig but they didn't receive it
// we resend the same updates and the same sig, and preserve the same ordering
val signedUpdates = commitments.changes.localChanges.signed
val commitSigs = commitments.active.flatMap(_.nextRemoteCommit_opt).map(_.sig)
val commitSigs = CommitSigs(commitments.active.flatMap(_.nextRemoteCommit_opt).map(_.sig))
retransmitRevocation_opt match {
case None =>
SyncResult.Success(retransmit = signedUpdates ++ commitSigs)
SyncResult.Success(retransmit = signedUpdates :+ commitSigs)
case Some(revocation) if commitments.localCommitIndex > waitingForRevocation.sentAfterLocalCommitIndex =>
SyncResult.Success(retransmit = signedUpdates ++ commitSigs ++ Seq(revocation))
SyncResult.Success(retransmit = signedUpdates :+ commitSigs :+ revocation)
case Some(revocation) =>
SyncResult.Success(retransmit = Seq(revocation) ++ signedUpdates ++ commitSigs)
SyncResult.Success(retransmit = revocation +: signedUpdates :+ commitSigs)
}
case Left(_) if remoteChannelReestablish.nextLocalCommitmentNumber == (commitments.nextRemoteCommitIndex + 1) =>
// we just sent a new commit_sig, they have received it but we haven't received their revocation
Expand Down
Loading