Skip to content

Commit f8a9860

Browse files
committed
Implement simple taproot channels
We add new commitment formats and TLV extensions to include musig2 nonces. This includes a specific commitment format for phoenix taproot channels. The old v1 channel establishment protocol is updated to include nonces and partial signatures. The v2 channel estalishment protocol, based on the interactive tx constuction protocol, is also updated, and the interactive tx session now includes: - an optional funding nonce for the shared input (i.e. the funding tx that is being spent) - a nonce for the commit tx that is being created, and another nonce that will become the channel's "next remote nonce" once the session completes The funding nonce is random and its lifecycle is bound to the interactive session. Side note: the new v2 protocol is both simpler to extend and gives us support for dual-funding and splices. Since there can be several different commitment transactions that valid at the same time while splices are pending, revoke_and_ack and channel_restablish are extended to include a list of funding_tx_id -> nonce tuples (one for each active commitment). channel_restablish also includes as an optional "current commit nonce": if we got disconnected while a splice was in progress before both nodes exchanged their commit signatures: if that is the case, we need to re-send our peer's current signature and will use this nonce to compute it. We also update the simple close protocol to include closing nonces. We allow upgrading channels to taproot during splices, with an optional channel_type TLV added to splice_init/splice_ack. This is not part of the BOLT proposal, and upgrading is currently limited to phoenix taproot channels from phoenix anchor channels.
1 parent 49bee72 commit f8a9860

File tree

51 files changed

+3465
-896
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3465
-896
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator._
4747
import fr.acinq.eclair.payment.send.{ClearRecipient, OfferPayment, PaymentIdentifier}
4848
import fr.acinq.eclair.router.Router
4949
import fr.acinq.eclair.router.Router._
50+
import fr.acinq.eclair.transactions.Transactions.CommitmentFormat
5051
import fr.acinq.eclair.wire.protocol.OfferTypes.Offer
5152
import fr.acinq.eclair.wire.protocol._
5253
import grizzled.slf4j.Logging
@@ -96,9 +97,9 @@ trait Eclair {
9697

9798
def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]]
9899

99-
def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
100+
def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
100101

101-
def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
102+
def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]]
102103

103104
def rbfSplice(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]]
104105

@@ -260,15 +261,15 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
260261
)
261262
}
262263

263-
override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
264+
override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
264265
val spliceIn = SpliceIn(additionalLocalFunding = amountIn, pushAmount = pushAmount_opt.getOrElse(0.msat))
265266
sendToChannelTyped(
266267
channel = Left(channelId),
267-
cmdBuilder = CMD_SPLICE(_, spliceIn_opt = Some(spliceIn), spliceOut_opt = None, requestFunding_opt = None)
268+
cmdBuilder = CMD_SPLICE(_, spliceIn_opt = Some(spliceIn), spliceOut_opt = None, requestFunding_opt = None, channelType_opt = channelType_opt)
268269
)
269270
}
270271

271-
override def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
272+
override def spliceOut(channelId: ByteVector32, amountOut: Satoshi, scriptOrAddress: Either[ByteVector, String], channelType_opt: Option[ChannelType])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
272273
val script = scriptOrAddress match {
273274
case Left(script) => script
274275
case Right(address) => addressToPublicKeyScript(this.appKit.nodeParams.chainHash, address) match {
@@ -279,7 +280,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
279280
val spliceOut = SpliceOut(amount = amountOut, scriptPubKey = script)
280281
sendToChannelTyped(
281282
channel = Left(channelId),
282-
cmdBuilder = CMD_SPLICE(_, spliceIn_opt = None, spliceOut_opt = Some(spliceOut), requestFunding_opt = None)
283+
cmdBuilder = CMD_SPLICE(_, spliceIn_opt = None, spliceOut_opt = Some(spliceOut), requestFunding_opt = None, channelType_opt = channelType_opt)
283284
)
284285
}
285286

eclair-core/src/main/scala/fr/acinq/eclair/Features.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,16 @@ object Features {
341341
val mandatory = 154
342342
}
343343

344+
case object SimpleTaprootChannelsPhoenix extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
345+
val rfcName = "option_simple_taproot_phoenix"
346+
val mandatory = 564
347+
}
348+
349+
case object SimpleTaprootChannelsStaging extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
350+
val rfcName = "option_simple_taproot_staging"
351+
val mandatory = 180
352+
}
353+
344354
/**
345355
* 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.
346356
* TODO: add NodeFeature once bLIP is merged.
@@ -384,6 +394,8 @@ object Features {
384394
ZeroConf,
385395
KeySend,
386396
SimpleClose,
397+
SimpleTaprootChannelsPhoenix,
398+
SimpleTaprootChannelsStaging,
387399
WakeUpNotificationClient,
388400
TrampolinePaymentPrototype,
389401
AsyncPaymentPrototype,
@@ -403,6 +415,8 @@ object Features {
403415
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
404416
KeySend -> (VariableLengthOnion :: Nil),
405417
SimpleClose -> (ShutdownAnySegwit :: Nil),
418+
SimpleTaprootChannelsPhoenix -> (ChannelType :: SimpleClose :: Nil),
419+
SimpleTaprootChannelsStaging -> (ChannelType :: SimpleClose :: Nil),
406420
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
407421
OnTheFlyFunding -> (SplicePrototype :: Nil),
408422
FundingFeeCredit -> (OnTheFlyFunding :: Nil)

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2020
import fr.acinq.bitcoin.scalacompat.Satoshi
2121
import fr.acinq.eclair.BlockHeight
2222
import fr.acinq.eclair.transactions.Transactions
23-
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, LegacySimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat}
23+
import fr.acinq.eclair.transactions.Transactions._
2424

2525
// @formatter:off
2626
sealed trait ConfirmationPriority extends Ordered[ConfirmationPriority] {
@@ -76,16 +76,16 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax
7676

7777
def isProposedFeerateTooHigh(commitmentFormat: CommitmentFormat, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
7878
commitmentFormat match {
79-
case Transactions.DefaultCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
80-
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | LegacySimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
79+
case Transactions.DefaultCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
80+
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => networkFeerate * ratioHigh < proposedFeerate
8181
}
8282
}
8383

8484
def isProposedFeerateTooLow(commitmentFormat: CommitmentFormat, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
8585
commitmentFormat match {
8686
case Transactions.DefaultCommitmentFormat => proposedFeerate < networkFeerate * ratioLow
8787
// When using anchor outputs, we allow low feerates: fees will be set with CPFP and RBF at broadcast time.
88-
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | LegacySimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => false
88+
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => false
8989
}
9090
}
9191
}
@@ -122,7 +122,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
122122

123123
commitmentFormat match {
124124
case Transactions.DefaultCommitmentFormat => networkFeerate
125-
case _: Transactions.AnchorOutputsCommitmentFormat | _: Transactions.SimpleTaprootChannelCommitmentFormat=>
125+
case _: Transactions.AnchorOutputsCommitmentFormat | _: Transactions.SimpleTaprootChannelCommitmentFormat =>
126126
val targetFeerate = networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
127127
// We make sure the feerate is always greater than the propagation threshold.
128128
targetFeerate.max(networkMinFee * 1.25)

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ sealed trait ChannelFundingCommand extends Command {
260260
}
261261
case class SpliceIn(additionalLocalFunding: Satoshi, pushAmount: MilliSatoshi = 0 msat)
262262
case class SpliceOut(amount: Satoshi, scriptPubKey: ByteVector)
263-
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], requestFunding_opt: Option[LiquidityAds.RequestFunding]) extends ChannelFundingCommand {
263+
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], requestFunding_opt: Option[LiquidityAds.RequestFunding], channelType_opt:Option[ChannelType]) extends ChannelFundingCommand {
264264
require(spliceIn_opt.isDefined || spliceOut_opt.isDefined, "there must be a splice-in or a splice-out")
265265
val additionalLocalFunding: Satoshi = spliceIn_opt.map(_.additionalLocalFunding).getOrElse(0 sat)
266266
val pushAmount: MilliSatoshi = spliceIn_opt.map(_.pushAmount).getOrElse(0 msat)

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,9 @@ case class ConcurrentRemoteSplice (override val channelId: Byte
154154
case class TooManySmallHtlcs (override val channelId: ByteVector32, number: Long, below: MilliSatoshi) extends ChannelJammingException(channelId, s"too many small htlcs: $number HTLCs below $below")
155155
case class IncomingConfidenceTooLow (override val channelId: ByteVector32, confidence: Double, occupancy: Double) extends ChannelJammingException(channelId, s"incoming confidence too low: confidence=$confidence occupancy=$occupancy")
156156
case class OutgoingConfidenceTooLow (override val channelId: ByteVector32, confidence: Double, occupancy: Double) extends ChannelJammingException(channelId, s"outgoing confidence too low: confidence=$confidence occupancy=$occupancy")
157+
case class MissingCommitNonce (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long) extends ChannelException(channelId, s"commit nonce for funding tx $fundingTxId and commitmentNumber=$commitmentNumber is missing")
158+
case class InvalidCommitNonce (override val channelId: ByteVector32, fundingTxId: TxId, commitmentNumber: Long) extends ChannelException(channelId, s"commit nonce for funding tx $fundingTxId and commitmentNumber=$commitmentNumber is not valid")
159+
case class MissingFundingNonce (override val channelId: ByteVector32, fundingTxId: TxId) extends ChannelException(channelId, s"funding nonce for funding tx $fundingTxId is missing")
160+
case class InvalidFundingNonce (override val channelId: ByteVector32, fundingTxId: TxId) extends ChannelException(channelId, s"funding nonce for funding tx $fundingTxId is not valid")
161+
case class MissingClosingNonce (override val channelId: ByteVector32) extends ChannelException(channelId, "closing nonce is missing")
157162
// @formatter:on

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package fr.acinq.eclair.channel
1818

19-
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat}
19+
import fr.acinq.eclair.transactions.Transactions._
2020
import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeature, PermanentChannelFeature}
2121

2222
/**
@@ -118,6 +118,29 @@ object ChannelTypes {
118118
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
119119
override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
120120
}
121+
case class SimpleTaprootChannelsPhoenix(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType {
122+
/** Known channel-type features */
123+
override def features: Set[ChannelTypeFeature] = Set(
124+
if (scidAlias) Some(Features.ScidAlias) else None,
125+
if (zeroConf) Some(Features.ZeroConf) else None,
126+
Some(Features.SimpleTaprootChannelsPhoenix),
127+
).flatten
128+
override def paysDirectlyToWallet: Boolean = false
129+
override def commitmentFormat: CommitmentFormat = PhoenixSimpleTaprootChannelCommitmentFormat
130+
override def toString: String = s"simple_taproot_channel_phoenix${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
131+
}
132+
case class SimpleTaprootChannelsStaging(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType {
133+
/** Known channel-type features */
134+
override def features: Set[ChannelTypeFeature] = Set(
135+
if (scidAlias) Some(Features.ScidAlias) else None,
136+
if (zeroConf) Some(Features.ZeroConf) else None,
137+
Some(Features.SimpleTaprootChannelsStaging),
138+
).flatten
139+
override def paysDirectlyToWallet: Boolean = false
140+
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat
141+
override def toString: String = s"simple_taproot_channel_staging${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
142+
}
143+
121144
case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType {
122145
override def features: Set[InitFeature] = featureBits.activated.keySet
123146
override def toString: String = s"0x${featureBits.toByteVector.toHex}"
@@ -140,7 +163,16 @@ object ChannelTypes {
140163
AnchorOutputsZeroFeeHtlcTx(),
141164
AnchorOutputsZeroFeeHtlcTx(zeroConf = true),
142165
AnchorOutputsZeroFeeHtlcTx(scidAlias = true),
143-
AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true))
166+
AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true),
167+
SimpleTaprootChannelsPhoenix(),
168+
SimpleTaprootChannelsPhoenix(zeroConf = true),
169+
SimpleTaprootChannelsPhoenix(scidAlias = true),
170+
SimpleTaprootChannelsPhoenix(scidAlias = true, zeroConf = true),
171+
SimpleTaprootChannelsStaging(),
172+
SimpleTaprootChannelsStaging(zeroConf = true),
173+
SimpleTaprootChannelsStaging(scidAlias = true),
174+
SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true),
175+
)
144176
.map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType)
145177
.toMap
146178

@@ -153,7 +185,11 @@ object ChannelTypes {
153185

154186
val scidAlias = canUse(Features.ScidAlias) && !announceChannel // alias feature is incompatible with public channel
155187
val zeroConf = canUse(Features.ZeroConf)
156-
if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) {
188+
if (canUse(Features.SimpleTaprootChannelsStaging)) {
189+
SimpleTaprootChannelsStaging(scidAlias, zeroConf)
190+
} else if (canUse(Features.SimpleTaprootChannelsPhoenix)) {
191+
SimpleTaprootChannelsPhoenix(scidAlias, zeroConf)
192+
} else if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) {
157193
AnchorOutputsZeroFeeHtlcTx(scidAlias, zeroConf)
158194
} else if (canUse(Features.AnchorOutputs)) {
159195
AnchorOutputs(scidAlias, zeroConf)

0 commit comments

Comments
 (0)