Skip to content

Commit dd34985

Browse files
committed
Implement simple taproot channels
This commit implements: - feature bits for simple taproot channels - TLV extensions for funding/closing wire messages - modifications to how we handle channel funding, splicing and mutual closing - changes to the commitment structures The v1 channel establishment protocol is modified to include nonces for creating and signing taproot transactions. This is bascially the original simple taproot proposal, which does not cover dual-funding, splices and rbf. We assume that simple taproot channels depends on the simple close protocol, which we extend to include musig2 nonces. Dual-funding, splices and rbf are supported by extending the interactive tx session protocol to include musig2 nonce, which are attached to the `tx_complete` message. There are 2 types of nonces: - "funding nonces", which are used to sign a new funding tx that spends the current funding tx (splice, rbf). - "commit nonces", which are used to sign the commit tx that is one of the outputs of the interactive session. "funding nonces" can be randomly generated on-the-fly: either the interactive session will fail, and they can be forgotten, or it will succeed and we'll get a new, fully signed funding tx. "commit nonces" can be deterministically generated. This make nonce exchange simpler to reason about: - when we send `tx_complete`, we know exactly what the funding tx and commit tx will be (so the funding tx id can be mixed in the nonce generation process). - dual funding, splice and rbf message do not need to be modified Channel re-establishment becomes a bit more complex, as one node could still be waiting for signatures while the other has completed the splice workflow, but it can be mitigated by storing the last sent commit_sig and re-sending it again if needed.
1 parent 402bfbb commit dd34985

Some content is hidden

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

41 files changed

+2489
-297
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,11 @@ object Features {
310310
val mandatory = 60
311311
}
312312

313+
case object SimpleTaproot extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
314+
val rfcName = "option_simple_taproot"
315+
val mandatory = 80
316+
}
317+
313318
/** This feature bit indicates that the node is a mobile wallet that can be woken up via push notifications. */
314319
case object WakeUpNotificationClient extends Feature with InitFeature {
315320
val rfcName = "wake_up_notification_client"
@@ -339,6 +344,11 @@ object Features {
339344
val mandatory = 154
340345
}
341346

347+
case object SimpleTaprootStaging extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
348+
val rfcName = "option_simple_taproot_staging"
349+
val mandatory = 182
350+
}
351+
342352
/**
343353
* 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.
344354
* TODO: add NodeFeature once bLIP is merged.
@@ -381,6 +391,8 @@ object Features {
381391
ZeroConf,
382392
KeySend,
383393
SimpleClose,
394+
SimpleTaproot,
395+
SimpleTaprootStaging,
384396
WakeUpNotificationClient,
385397
TrampolinePaymentPrototype,
386398
AsyncPaymentPrototype,
@@ -400,6 +412,8 @@ object Features {
400412
TrampolinePaymentPrototype -> (PaymentSecret :: Nil),
401413
KeySend -> (VariableLengthOnion :: Nil),
402414
SimpleClose -> (ShutdownAnySegwit :: Nil),
415+
SimpleTaproot -> (ChannelType :: SimpleClose :: Nil),
416+
SimpleTaprootStaging -> (ChannelType :: SimpleClose :: Nil),
403417
AsyncPaymentPrototype -> (TrampolinePaymentPrototype :: Nil),
404418
OnTheFlyFunding -> (SplicePrototype :: Nil),
405419
FundingFeeCredit -> (OnTheFlyFunding :: Nil)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import fr.acinq.eclair.router.Router._
3939
import fr.acinq.eclair.router.{Graph, PathFindingExperimentConf, Router}
4040
import fr.acinq.eclair.tor.Socks5ProxyParams
4141
import fr.acinq.eclair.transactions.Transactions
42+
import fr.acinq.eclair.transactions.Transactions.SimpleTaprootChannelCommitmentFormat
4243
import fr.acinq.eclair.wire.protocol._
4344
import grizzled.slf4j.Logging
4445
import scodec.bits.ByteVector
@@ -134,7 +135,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
134135
min = (commitmentFeerate * feerateTolerance.ratioLow).max(minimumFeerate),
135136
max = (commitmentFormat match {
136137
case Transactions.DefaultCommitmentFormat => commitmentFeerate * feerateTolerance.ratioHigh
137-
case _: Transactions.AnchorOutputsCommitmentFormat | Transactions.SimpleTaprootChannelCommitmentFormat => (commitmentFeerate * feerateTolerance.ratioHigh).max(feerateTolerance.anchorOutputMaxCommitFeerate)
138+
case _: Transactions.AnchorOutputsCommitmentFormat | SimpleTaprootChannelCommitmentFormat => (commitmentFeerate * feerateTolerance.ratioHigh).max(feerateTolerance.anchorOutputMaxCommitFeerate)
138139
}).max(minimumFeerate),
139140
)
140141
RecommendedFeerates(chainHash, fundingFeerate, commitmentFeerate, TlvStream(fundingRange, commitmentRange))

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package fr.acinq.eclair.channel
1818

1919
import akka.actor.{ActorRef, PossiblyHarmful, typed}
20+
import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce}
2021
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2122
import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxId, TxOut}
2223
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
@@ -551,7 +552,7 @@ sealed trait ChannelDataWithCommitments extends PersistentChannelData {
551552
final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_CHANNEL_NON_INITIATOR) extends TransientChannelData {
552553
val channelId: ByteVector32 = initFundee.temporaryChannelId
553554
}
554-
final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel) extends TransientChannelData {
555+
final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel, nextLocalNonce: Option[kotlin.Pair[SecretNonce, IndividualNonce]] = None) extends TransientChannelData {
555556
val channelId: ByteVector32 = initFunder.temporaryChannelId
556557
}
557558
final case class DATA_WAIT_FOR_FUNDING_INTERNAL(params: ChannelParams,
@@ -568,7 +569,8 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(params: ChannelParams,
568569
pushAmount: MilliSatoshi,
569570
commitTxFeerate: FeeratePerKw,
570571
remoteFundingPubKey: PublicKey,
571-
remoteFirstPerCommitmentPoint: PublicKey) extends TransientChannelData {
572+
remoteFirstPerCommitmentPoint: PublicKey,
573+
remoteNextLocalNonce: Option[IndividualNonce]) extends TransientChannelData {
572574
val channelId: ByteVector32 = params.channelId
573575
}
574576
final case class DATA_WAIT_FOR_FUNDING_SIGNED(params: ChannelParams,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,5 @@ case class CommandUnavailableInThisState (override val channelId: Byte
150150
case class ForbiddenDuringSplice (override val channelId: ByteVector32, command: String) extends ChannelException(channelId, s"cannot process $command while splicing")
151151
case class ForbiddenDuringQuiescence (override val channelId: ByteVector32, command: String) extends ChannelException(channelId, s"cannot process $command while quiescent")
152152
case class ConcurrentRemoteSplice (override val channelId: ByteVector32) extends ChannelException(channelId, "splice attempt canceled, remote initiated splice before us")
153+
case class MissingNextLocalNonce (override val channelId: ByteVector32) extends ChannelException(channelId, "next local nonce tlv is missing")
153154
// @formatter:on

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

Lines changed: 29 additions & 6 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
/**
@@ -31,9 +31,11 @@ import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeatur
3131
case class ChannelFeatures(features: Set[PermanentChannelFeature]) {
3232

3333
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
34-
val paysDirectlyToWallet: Boolean = hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs) && !hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)
34+
val paysDirectlyToWallet: Boolean = hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs) && !hasFeature(Features.AnchorOutputsZeroFeeHtlcTx) && !hasFeature((Features.SimpleTaprootStaging))
3535
/** Legacy option_anchor_outputs is used for Phoenix, because Phoenix doesn't have an on-chain wallet to pay for fees. */
36-
val commitmentFormat: CommitmentFormat = if (hasFeature(Features.AnchorOutputs)) {
36+
val commitmentFormat: CommitmentFormat = if (hasFeature(Features.SimpleTaprootStaging)) {
37+
SimpleTaprootChannelCommitmentFormat
38+
} else if (hasFeature(Features.AnchorOutputs)) {
3739
UnsafeLegacyAnchorOutputsCommitmentFormat
3840
} else if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) {
3941
ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
@@ -129,6 +131,18 @@ object ChannelTypes {
129131
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
130132
override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
131133
}
134+
case class SimpleTaprootChannelsStaging(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType {
135+
/** Known channel-type features */
136+
override def features: Set[ChannelTypeFeature] = Set(
137+
if (scidAlias) Some(Features.ScidAlias) else None,
138+
if (zeroConf) Some(Features.ZeroConf) else None,
139+
Some(Features.SimpleTaprootStaging),
140+
).flatten
141+
override def paysDirectlyToWallet: Boolean = false
142+
override def commitmentFormat: CommitmentFormat = SimpleTaprootChannelCommitmentFormat
143+
override def toString: String = s"simple_taproot_channel_staging${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
144+
}
145+
132146
case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType {
133147
override def features: Set[InitFeature] = featureBits.activated.keySet
134148
override def toString: String = s"0x${featureBits.toByteVector.toHex}"
@@ -151,20 +165,29 @@ object ChannelTypes {
151165
AnchorOutputsZeroFeeHtlcTx(),
152166
AnchorOutputsZeroFeeHtlcTx(zeroConf = true),
153167
AnchorOutputsZeroFeeHtlcTx(scidAlias = true),
154-
AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true))
168+
AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true),
169+
SimpleTaprootChannelsStaging(),
170+
SimpleTaprootChannelsStaging(zeroConf = true),
171+
SimpleTaprootChannelsStaging(scidAlias = true),
172+
SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true),
173+
)
155174
.map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType)
156175
.toMap
157176

158177
// NB: Bolt 2: features must exactly match in order to identify a channel type.
159-
def fromFeatures(features: Features[InitFeature]): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features))
178+
def fromFeatures(features: Features[InitFeature]): ChannelType = {
179+
features2ChannelType.getOrElse(features, UnsupportedChannelType(features))
180+
}
160181

161182
/** Pick the channel type based on local and remote feature bits, as defined by the spec. */
162183
def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): SupportedChannelType = {
163184
def canUse(feature: InitFeature): Boolean = Features.canUseFeature(localFeatures, remoteFeatures, feature)
164185

165186
val scidAlias = canUse(Features.ScidAlias) && !announceChannel // alias feature is incompatible with public channel
166187
val zeroConf = canUse(Features.ZeroConf)
167-
if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) {
188+
if (canUse(Features.SimpleTaprootStaging)) {
189+
SimpleTaprootChannelsStaging(scidAlias, zeroConf)
190+
} else if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) {
168191
AnchorOutputsZeroFeeHtlcTx(scidAlias, zeroConf)
169192
} else if (canUse(Features.AnchorOutputs)) {
170193
AnchorOutputs(scidAlias, zeroConf)

0 commit comments

Comments
 (0)