Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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:
Comment thread
remyers marked this conversation as resolved.

```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
10 changes: 7 additions & 3 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 @@ -482,6 +486,7 @@ object Features {
ZeroConf,
KeySend,
SimpleClose,
Splicing,
SimpleTaprootChannelsPhoenix,
SimpleTaprootChannelsStaging,
WakeUpNotificationClient,
Expand All @@ -506,7 +511,6 @@ object Features {
SimpleClose -> (ShutdownAnySegwit :: Nil),
SimpleTaprootChannelsPhoenix -> (ChannelType :: SimpleClose :: Nil),
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
OnTheFlyFunding -> (SplicePrototype :: Nil),
FundingFeeCredit -> (OnTheFlyFunding :: Nil)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ case class ChannelParams(channelId: ByteVector32,
val remoteNodeId: PublicKey = remoteParams.nodeId
// If we've set the 0-conf feature bit for this peer, we will always use 0-conf with them.
val zeroConf: Boolean = localParams.initFeatures.hasFeature(Features.ZeroConf)
// TODO: we keep supporting the legacy splicing protocol for non-upgraded Phoenix users.
lazy val useLegacySpliceProtocol = remoteParams.initFeatures.hasFeature(Features.SplicePrototype)

/** We update local/global features at reconnection. */
def updateFeatures(localInit: Init, remoteInit: Init): ChannelParams = copy(
Expand Down
194 changes: 131 additions & 63 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,10 +1206,14 @@ 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

// For the legacy splice protocol, we use the next_commitment_number to let our peer know whether they needed to
// retransmit commit_sig or not. We're now using an explicit bit instead, but need to maintain backwards-compatibility.
def nextLocalCommitmentNumber(useLegacySpliceProtocol: Boolean): Long = localCommit match {
case Left(unsignedCommit) if useLegacySpliceProtocol => unsignedCommit.index
case _ => localCommitIndex + 1
}

def localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex)
Expand Down
22 changes: 20 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,20 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
stay()

case Event(msg: LightningMessage, d: ConnectedData) if sender() != d.transport => // if the message doesn't originate from the transport, it is an outgoing message
val useExperimentalSplice = d.remoteInit.features.hasFeature(Features.SplicePrototype)
msg match {
// If our peer is using the experimental splice version, we convert splice messages.
case msg: SpliceInit if useExperimentalSplice => d.transport forward ExperimentalSpliceInit.from(msg)
case msg: SpliceAck if useExperimentalSplice => d.transport forward ExperimentalSpliceAck.from(msg)
case msg: SpliceLocked if useExperimentalSplice => d.transport forward ExperimentalSpliceLocked.from(msg)
case msg: TxAddInput if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[TxAddInputTlv.SharedInputTxId])))
case msg: TxSignatures if useExperimentalSplice => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[TxSignaturesTlv.PreviousFundingTxSig])))
case batch: CommitSigBatch if useExperimentalSplice => batch.messages.foreach(msg => d.transport forward msg.copy(tlvStream = TlvStream(msg.tlvStream.records.filterNot(_.isInstanceOf[CommitSigTlv.FundingTx]))))
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 @@ -426,6 +434,16 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
d.peer ! msg
stay()
}
// If our peer is using the experimental splice version, we convert splice messages.
case msg: ExperimentalSpliceInit =>
d.peer ! msg.toSpliceInit
stay()
case msg: ExperimentalSpliceAck =>
d.peer ! msg.toSpliceAck
stay()
case msg: ExperimentalSpliceLocked =>
d.peer ! msg.toSpliceLocked
stay()
case _ =>
d.peer ! msg
stay()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package fr.acinq.eclair.wire.protocol

import fr.acinq.bitcoin.scalacompat.Musig2.IndividualNonce
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, TxId}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, TxHash, TxId}
import fr.acinq.eclair.channel.ChannelSpendSignature.PartialSignatureWithNonce
import fr.acinq.eclair.channel.{ChannelType, ChannelTypes}
import fr.acinq.eclair.wire.protocol.CommonCodecs._
Expand Down Expand Up @@ -255,17 +255,48 @@ sealed trait ChannelReestablishTlv extends Tlv

object ChannelReestablishTlv {

/** TODO: replaced by [[NextFundingTlv]], remove once Phoenix users have upgraded. */
case class ExperimentalNextFundingTlv(txId: TxId) extends ChannelReestablishTlv

/**
* We unfortunately have a conflict between the splicing protocol we use for Phoenix and the official one.
* The official protocol uses TLV = 1 for the next_funding TLV, which should be implemented by [[NextFundingTlv]]
* (which is commented out below).
* The splicing protocol used for Phoenix also uses TLV = 1, but for a different TLV that was removed from the
* official splicing protocol (your_last_funding_locked), which contained the txid of the last [[ChannelReady]] or
* [[SpliceLocked]] message received before disconnecting, if any.
*
* To guarantee backwards-compatibility, we create a TLV field that may contain both options. When using the official
* splicing protocol, it will contain 33 bytes (a txid and a bitfield), while when using the legacy protocol it only
* contains a txid, which lets us easily distinguish the two.
*
* TODO: once we can remove support for Phoenix users with the legacy splicing protocol, this should just be replaced
* by [[NextFundingTlv]] which is commented out below and should be uncommented.
*/
case class NextFundingOrExperimentalYourLastFundingLockedTlv(data: ByteVector) extends ChannelReestablishTlv {
val isOfficial: Boolean = data.size == 33
val txId: TxId = TxId(TxHash(ByteVector32(data.take(32))))
// NB: this is only used for the official splicing protocol.
val retransmitCommitSig: Boolean = if (isOfficial) (data.last.toInt % 2) == 1 else false
}

/**
* 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
/** TODO: replaced by [[MyCurrentFundingLockedTlv]], remove once Phoenix users have upgraded. */
case class ExperimentalMyCurrentFundingLockedTlv(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 @@ -280,16 +311,31 @@ object ChannelReestablishTlv {
*/
case class NextLocalNoncesTlv(nonces: Seq[(TxId, IndividualNonce)]) extends ChannelReestablishTlv

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

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

object NextFundingOrExperimentalYourLastFundingLockedTlv {
def asNextFunding(txId: TxId, retransmitCommitSig: Boolean): NextFundingOrExperimentalYourLastFundingLockedTlv = {
val retransmitFlags = if (retransmitCommitSig) ByteVector.fromValidHex("01") else ByteVector.fromValidHex("00")
NextFundingOrExperimentalYourLastFundingLockedTlv(TxHash(txId).value ++ retransmitFlags)
}

def asExperimentalYourLastFundingLocked(txId: TxId): NextFundingOrExperimentalYourLastFundingLockedTlv = NextFundingOrExperimentalYourLastFundingLockedTlv(TxHash(txId).value)

val codec: Codec[NextFundingOrExperimentalYourLastFundingLockedTlv] = tlvField(bytes)
}

object YourLastFundingLockedTlv {
val codec: Codec[YourLastFundingLockedTlv] = tlvField("your_last_funding_locked_txid" | txIdAsHash)
object ExperimentalMyCurrentFundingLockedTlv {
val codec: Codec[ExperimentalMyCurrentFundingLockedTlv] = tlvField("my_current_funding_locked_txid" | txIdAsHash)
}

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 +347,12 @@ 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(0), ExperimentalNextFundingTlv.codec)
// TODO: replace with the commented line below when removing support for the legacy splicing protocol.
.typecase(UInt64(1), NextFundingOrExperimentalYourLastFundingLockedTlv.codec)
// .typecase(UInt64(1), NextFundingTlv.codec)
.typecase(UInt64(3), ExperimentalMyCurrentFundingLockedTlv.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 @@ -36,6 +36,9 @@ object TxAddInputTlv {
/** When doing a splice, the initiator must provide the previous funding txId instead of the whole transaction. */
case class SharedInputTxId(txId: TxId) extends TxAddInputTlv

/** Same as [[SharedInputTxId]] for peers who only support the experimental version of splicing. */
case class ExperimentalSharedInputTxId(txId: TxId) extends TxAddInputTlv

/**
* When creating an interactive-tx where both participants sign a taproot input, we don't need to provide the entire
* previous transaction in [[TxAddInput]]: signatures will commit to the txOut of *all* of the transaction's inputs,
Expand All @@ -49,7 +52,8 @@ 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(1105), tlvField(txIdAsHash.as[ExperimentalSharedInputTxId]))
.typecase(UInt64(1111), PrevTxOut.codec)
)
}
Expand Down Expand Up @@ -102,9 +106,13 @@ object TxSignaturesTlv {
/** When doing a splice for a taproot channel, each peer must provide their partial signature for the previous musig2 funding output. */
case class PreviousFundingTxPartialSig(partialSigWithNonce: PartialSignatureWithNonce) extends TxSignaturesTlv

/** Same as [[PreviousFundingTxSig]] for peers who only support the experimental version of splicing. */
case class ExperimentalPreviousFundingTxSig(sig: ByteVector64) 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]))
.typecase(UInt64(601), tlvField(bytes64.as[ExperimentalPreviousFundingTxSig]))
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,17 +437,36 @@ object LightningMessageCodecs {
("fundingPubkey" | publicKey) ::
("tlvStream" | SpliceInitTlv.spliceInitTlvCodec)).as[SpliceInit]

val experimentalSpliceInitCodec: Codec[ExperimentalSpliceInit] = (
("channelId" | bytes32) ::
("fundingContribution" | satoshiSigned) ::
("feerate" | feeratePerKw) ::
("lockTime" | uint32) ::
("fundingPubkey" | publicKey) ::
("tlvStream" | SpliceInitTlv.spliceInitTlvCodec)).as[ExperimentalSpliceInit]

val spliceAckCodec: Codec[SpliceAck] = (
("channelId" | bytes32) ::
("fundingContribution" | satoshiSigned) ::
("fundingPubkey" | publicKey) ::
("tlvStream" | SpliceAckTlv.spliceAckTlvCodec)).as[SpliceAck]

val experimentalSpliceAckCodec: Codec[ExperimentalSpliceAck] = (
("channelId" | bytes32) ::
("fundingContribution" | satoshiSigned) ::
("fundingPubkey" | publicKey) ::
("tlvStream" | SpliceAckTlv.spliceAckTlvCodec)).as[ExperimentalSpliceAck]

val spliceLockedCodec: Codec[SpliceLocked] = (
("channelId" | bytes32) ::
("fundingTxHash" | txIdAsHash) ::
("tlvStream" | SpliceLockedTlv.spliceLockedTlvCodec)).as[SpliceLocked]

val experimentalSpliceLockedCodec: Codec[ExperimentalSpliceLocked] = (
("channelId" | bytes32) ::
("fundingTxHash" | txIdAsHash) ::
("tlvStream" | SpliceLockedTlv.spliceLockedTlvCodec)).as[ExperimentalSpliceLocked]

val stfuCodec: Codec[Stfu] = (
("channelId" | bytes32) ::
("initiator" | byte.xmap[Boolean](b => b != 0, b => if (b) 1 else 0))).as[Stfu]
Expand Down Expand Up @@ -530,6 +549,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 +584,9 @@ object LightningMessageCodecs {
.typecase(41045, addFeeCreditCodec)
.typecase(41046, currentFeeCreditCodec)
//
.typecase(37000, spliceInitCodec)
.typecase(37002, spliceAckCodec)
.typecase(37004, spliceLockedCodec)
.typecase(37000, experimentalSpliceInitCodec)
.typecase(37002, experimentalSpliceAckCodec)
.typecase(37004, experimentalSpliceLockedCodec)
//

//
Expand Down
Loading
Loading