Skip to content
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 @@ -9,6 +9,39 @@
With this release, eclair requires using Bitcoin Core 30.x.
Newer versions of Bitcoin Core may be used, but have not been extensively tested.

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

### Remove support for non-anchor channels

We remove the code used to support legacy channels that don't use anchor outputs or taproot.
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 @@ -88,6 +88,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 @@ -344,8 +344,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 @@ -395,6 +394,11 @@ object Features {
val mandatory = 60
}

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

case object PhoenixZeroReserve extends Feature with InitFeature with ChannelTypeFeature with PermanentChannelFeature {
val rfcName = "phoenix_zero_reserve"
val mandatory = 128
Expand Down Expand Up @@ -423,12 +427,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
}

case object SimpleTaprootChannelsPhoenix extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
val rfcName = "option_simple_taproot_phoenix"
val mandatory = 564
Expand Down Expand Up @@ -482,12 +480,12 @@ object Features {
ZeroConf,
KeySend,
SimpleClose,
Splicing,
SimpleTaprootChannelsPhoenix,
SimpleTaprootChannelsStaging,
WakeUpNotificationClient,
TrampolinePaymentPrototype,
AsyncPaymentPrototype,
SplicePrototype,
OnTheFlyFunding,
FundingFeeCredit,
PhoenixZeroReserve
Expand All @@ -506,7 +504,6 @@ object Features {
SimpleClose -> (ShutdownAnySegwit :: Nil),
SimpleTaprootChannelsPhoenix -> (ChannelType :: SimpleClose :: Nil),
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
OnTheFlyFunding -> (SplicePrototype :: Nil),
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
)

Expand Down
147 changes: 49 additions & 98 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1206,11 +1206,8 @@ object InteractiveTxSigningSession {
liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession {
val fundingTxId: TxId = fundingTx.txId
val localCommitIndex: Long = localCommit.fold(_.index, _.index)
// This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not.
val nextLocalCommitmentNumber: Long = localCommit match {
case Left(unsignedCommit) => unsignedCommit.index
case Right(commit) => commit.index + 1
}
// If we haven't received the remote commit_sig, we will request a retransmission on reconnection.
val retransmitRemoteCommitSig: Boolean = localCommit.isLeft

def localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex)

Expand Down
40 changes: 2 additions & 38 deletions eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
case batch: CommitSigBatch =>
// We insert a start_batch message to let our peer know how many commit_sig they will receive.
d.transport forward StartBatch.commitSigBatch(batch.channelId, batch.batchSize)
batch.messages.foreach(msg => d.transport forward msg)
case msg => d.transport forward msg
batch.messages.foreach(msg => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.ExperimentalBatchTlv]))))
case _ => d.transport forward msg
}
msg match {
// If we send any channel management message to this peer, the connection should be persistent.
Expand Down Expand Up @@ -391,41 +391,6 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
d.peer ! msg
stay() using d.copy(commitSigBatch_opt = None)
}
case msg: CommitSig =>
// We keep supporting the experimental version of splicing that older Phoenix wallets use.
// Once we're confident that enough Phoenix users have upgraded, we should remove this branch.
msg.tlvStream.get[CommitSigTlv.ExperimentalBatchTlv].map(_.size) match {
case Some(batchSize) if batchSize > 25 =>
log.warning("received legacy batch of commit_sig exceeding our threshold ({} > 25), processing messages individually", batchSize)
// We don't want peers to be able to exhaust our memory by sending batches of dummy messages that we keep in RAM.
d.peer ! msg
stay()
case Some(batchSize) if batchSize > 1 =>
d.legacyCommitSigBatch_opt match {
case Some(pending) if pending.channelId != msg.channelId || pending.batchSize != batchSize =>
log.warning("received invalid commit_sig batch while a different batch isn't complete")
// This should never happen, otherwise it will likely lead to a force-close.
d.peer ! CommitSigBatch(pending.received)
stay() using d.copy(legacyCommitSigBatch_opt = Some(PendingCommitSigBatch(msg.channelId, batchSize, Seq(msg))))
case Some(pending) =>
val received1 = pending.received :+ msg
if (received1.size == batchSize) {
log.debug("received last commit_sig in legacy batch for channel_id={}", msg.channelId)
d.peer ! CommitSigBatch(received1)
stay() using d.copy(legacyCommitSigBatch_opt = None)
} else {
log.debug("received commit_sig {}/{} in legacy batch for channel_id={}", received1.size, batchSize, msg.channelId)
stay() using d.copy(legacyCommitSigBatch_opt = Some(pending.copy(received = received1)))
}
case None =>
log.debug("received first commit_sig in legacy batch of size {} for channel_id={}", batchSize, msg.channelId)
stay() using d.copy(legacyCommitSigBatch_opt = Some(PendingCommitSigBatch(msg.channelId, batchSize, Seq(msg))))
}
case _ =>
log.debug("received individual commit_sig for channel_id={}", msg.channelId)
d.peer ! msg
stay()
}
case _ =>
d.peer ! msg
stay()
Expand Down Expand Up @@ -659,7 +624,6 @@ object PeerConnection {
behavior: Behavior = Behavior(),
expectedPong_opt: Option[ExpectedPong] = None,
commitSigBatch_opt: Option[PendingCommitSigBatch] = None,
legacyCommitSigBatch_opt: Option[PendingCommitSigBatch] = None,
isPersistent: Boolean) extends Data with HasTransport

case class PendingCommitSigBatch(channelId: ByteVector32, batchSize: Int, received: Seq[CommitSig])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,17 @@ object ChannelReestablishTlv {
/**
* When disconnected in the middle of an interactive-tx session, this field is used to request a retransmission of
* [[TxSignatures]] for the given [[txId]].
*
* @param txId the txid of the partially signed funding transaction.
* @param retransmitCommitSig true if [[CommitSig]] must be retransmitted before [[TxSignatures]].
*/
case class NextFundingTlv(txId: TxId) extends ChannelReestablishTlv
case class NextFundingTlv(txId: TxId, retransmitCommitSig: Boolean) extends ChannelReestablishTlv

/** The txid of the last [[ChannelReady]] or [[SpliceLocked]] message received before disconnecting, if any. */
case class YourLastFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv

/** The txid of our latest outgoing [[ChannelReady]] or [[SpliceLocked]] for this channel. */
case class MyCurrentFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
/**
* @param txId the txid of our latest outgoing [[ChannelReady]] or [[SpliceLocked]] for this channel.
* @param retransmitAnnSigs true if [[AnnouncementSignatures]] must be retransmitted.
*/
case class MyCurrentFundingLockedTlv(txId: TxId, retransmitAnnSigs: Boolean) extends ChannelReestablishTlv

/**
* When disconnected during an interactive tx session, we'll include a verification nonce for our *current* commitment
Expand All @@ -281,15 +284,11 @@ object ChannelReestablishTlv {
case class NextLocalNoncesTlv(nonces: Seq[(TxId, IndividualNonce)]) extends ChannelReestablishTlv

object NextFundingTlv {
val codec: Codec[NextFundingTlv] = tlvField(txIdAsHash)
}

object YourLastFundingLockedTlv {
val codec: Codec[YourLastFundingLockedTlv] = tlvField("your_last_funding_locked_txid" | txIdAsHash)
val codec: Codec[NextFundingTlv] = tlvField(("next_funding_txid" | txIdAsHash) :: ("retransmit_flags" | (ignore(7) :: bool)))
}

object MyCurrentFundingLockedTlv {
val codec: Codec[MyCurrentFundingLockedTlv] = tlvField("my_current_funding_locked_txid" | txIdAsHash)
val codec: Codec[MyCurrentFundingLockedTlv] = tlvField(("my_current_funding_locked_txid" | txIdAsHash) :: ("retransmit_flags" | (ignore(7) :: bool)))
}

object CurrentCommitNonceTlv {
Expand All @@ -301,9 +300,8 @@ object ChannelReestablishTlv {
}

val channelReestablishTlvCodec: Codec[TlvStream[ChannelReestablishTlv]] = tlvStream(discriminated[ChannelReestablishTlv].by(varint)
.typecase(UInt64(0), NextFundingTlv.codec)
.typecase(UInt64(1), YourLastFundingLockedTlv.codec)
.typecase(UInt64(3), MyCurrentFundingLockedTlv.codec)
.typecase(UInt64(1), NextFundingTlv.codec)
.typecase(UInt64(5), MyCurrentFundingLockedTlv.codec)
.typecase(UInt64(22), NextLocalNoncesTlv.codec)
.typecase(UInt64(24), CurrentCommitNonceTlv.codec)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ object TxAddInputTlv {

val txAddInputTlvCodec: Codec[TlvStream[TxAddInputTlv]] = tlvStream(discriminated[TxAddInputTlv].by(varint)
// Note that we actually encode as a tx_hash to be consistent with other lightning messages.
.typecase(UInt64(1105), tlvField(txIdAsHash.as[SharedInputTxId]))
.typecase(UInt64(0), tlvField(txIdAsHash.as[SharedInputTxId]))
.typecase(UInt64(1111), PrevTxOut.codec)
)
}
Expand Down Expand Up @@ -103,8 +103,8 @@ object TxSignaturesTlv {
case class PreviousFundingTxPartialSig(partialSigWithNonce: PartialSignatureWithNonce) extends TxSignaturesTlv

val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint)
.typecase(UInt64(0), tlvField(bytes64.as[PreviousFundingTxSig]))
.typecase(UInt64(2), tlvField(partialSignatureWithNonce.as[PreviousFundingTxPartialSig]))
.typecase(UInt64(601), tlvField(bytes64.as[PreviousFundingTxSig]))
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,9 @@ object LightningMessageCodecs {
.typecase(72, txInitRbfCodec)
.typecase(73, txAckRbfCodec)
.typecase(74, txAbortCodec)
.typecase(77, spliceLockedCodec)
.typecase(80, spliceInitCodec)
.typecase(81, spliceAckCodec)
.typecase(127, startBatchCodec)
.typecase(128, updateAddHtlcCodec)
.typecase(130, updateFulfillHtlcCodec)
Expand Down Expand Up @@ -562,9 +565,6 @@ object LightningMessageCodecs {
.typecase(41045, addFeeCreditCodec)
.typecase(41046, currentFeeCreditCodec)
//
.typecase(37000, spliceInitCodec)
.typecase(37002, spliceAckCodec)
.typecase(37004, spliceLockedCodec)
//

//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,9 @@ case class ChannelReestablish(channelId: ByteVector32,
myCurrentPerCommitmentPoint: PublicKey,
tlvStream: TlvStream[ChannelReestablishTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].map(_.txId)
val retransmitInteractiveTxCommitSig: Boolean = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].exists(_.retransmitCommitSig)
val myCurrentFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].map(_.txId)
val yourLastFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.YourLastFundingLockedTlv].map(_.txId)
val retransmitAnnSigs: Boolean = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].exists(_.retransmitAnnSigs)
val nextCommitNonces: Map[TxId, IndividualNonce] = tlvStream.get[ChannelReestablishTlv.NextLocalNoncesTlv].map(_.nonces.toMap).getOrElse(Map.empty)
val currentCommitNonce_opt: Option[IndividualNonce] = tlvStream.get[ChannelReestablishTlv.CurrentCommitNonceTlv].map(_.nonce)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"initFeatures" : {
"activated" : {
"option_route_blinding" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand All @@ -41,7 +40,6 @@
"activated" : {
"option_route_blinding" : "optional",
"option_provide_storage" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"activated" : {
"option_route_blinding" : "optional",
"option_provide_storage" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand All @@ -41,7 +40,6 @@
"initFeatures" : {
"activated" : {
"option_route_blinding" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"activated" : {
"option_route_blinding" : "optional",
"option_provide_storage" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand All @@ -41,7 +40,6 @@
"initFeatures" : {
"activated" : {
"option_route_blinding" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"activated" : {
"option_route_blinding" : "optional",
"option_dual_fund" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand All @@ -40,7 +39,6 @@
"option_route_blinding" : "optional",
"option_provide_storage" : "optional",
"option_dual_fund" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"option_route_blinding" : "optional",
"option_provide_storage" : "optional",
"option_dual_fund" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand All @@ -40,7 +39,6 @@
"activated" : {
"option_route_blinding" : "optional",
"option_dual_fund" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"activated" : {
"option_route_blinding" : "optional",
"option_dual_fund" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand All @@ -41,7 +40,6 @@
"option_route_blinding" : "optional",
"option_provide_storage" : "optional",
"option_dual_fund" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"option_route_blinding" : "optional",
"option_provide_storage" : "optional",
"option_dual_fund" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand All @@ -41,7 +40,6 @@
"activated" : {
"option_route_blinding" : "optional",
"option_dual_fund" : "optional",
"splice_prototype" : "optional",
"payment_secret" : "mandatory",
"gossip_queries_ex" : "optional",
"option_anchor_outputs" : "optional",
Expand Down
Loading
Loading