Skip to content

Commit 29aed0d

Browse files
committed
Add support for taproot zero-fee commitment format
We add support for the zero-fee commitment format specified in lightning/bolts#1330. Channels using this commitment format benefit from better protection against pinning attacks (thanks to TRUC/v3 transactions), don't need the `update_fee` mechanism, have less dust exposure risk, and use an overall simpler state machine, while benefiting from the privacy improvements of taproot channels.
1 parent af4a67f commit 29aed0d

18 files changed

Lines changed: 233 additions & 126 deletions

File tree

eclair-core/src/main/resources/reference.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ eclair {
7777
option_attribution_data = optional
7878
option_onion_messages = optional
7979
zero_fee_commitments = disabled
80+
taproot_zero_fee_commitments_staging = disabled
8081
// This feature should only be enabled when acting as an LSP for mobile wallets.
8182
// When activating this feature, the peer-storage section should be customized to match desired SLAs.
8283
option_provide_storage = disabled

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,11 @@ object Features {
445445
val mandatory = 180
446446
}
447447

448+
case object TaprootZeroFeeCommitmentsStaging extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
449+
val rfcName = "taproot_zero_fee_commitments_staging"
450+
val mandatory = 182
451+
}
452+
448453
/**
449454
* 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.
450455
* TODO: add NodeFeature once bLIP is merged.
@@ -491,6 +496,7 @@ object Features {
491496
SimpleClose,
492497
SimpleTaprootChannelsPhoenix,
493498
SimpleTaprootChannelsStaging,
499+
TaprootZeroFeeCommitmentsStaging,
494500
WakeUpNotificationClient,
495501
TrampolinePaymentPrototype,
496502
AsyncPaymentPrototype,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
9393
case Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | Transactions.ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat =>
9494
// If the fee has a large enough change, we update the fee.
9595
currentFeeratePerKw.toLong == 0 || Math.abs((currentFeeratePerKw.toLong - nextFeeratePerKw.toLong).toDouble / currentFeeratePerKw.toLong) > updateFeeMinDiffRatio
96-
case Transactions.ZeroFeeCommitmentFormat =>
96+
case Transactions.ZeroFeeCommitmentFormat | Transactions.TaprootZeroFeeCommitmentFormat =>
9797
// We never send update_fee when using zero-fee commitments.
9898
false
9999
}
@@ -114,7 +114,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
114114
val networkFeerate = feerates.fast
115115
val networkMinFee = feerates.minimum
116116
commitmentFormat match {
117-
case Transactions.ZeroFeeCommitmentFormat => FeeratePerKw(0 sat)
117+
case Transactions.ZeroFeeCommitmentFormat | Transactions.TaprootZeroFeeCommitmentFormat => FeeratePerKw(0 sat)
118118
case Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat | Transactions.PhoenixSimpleTaprootChannelCommitmentFormat =>
119119
// Since Bitcoin Core v28, 1-parent-1-child package relay has been deployed: it should be ok if the commit tx
120120
// doesn't propagate on its own.

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ object ChannelTypes {
113113
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat
114114
override def toString: String = s"simple_taproot_channel_staging${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
115115
}
116+
case class TaprootZeroFeeCommitments(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType {
117+
override def features: Set[ChannelTypeFeature] = Set(
118+
if (scidAlias) Some(Features.ScidAlias) else None,
119+
if (zeroConf) Some(Features.ZeroConf) else None,
120+
Some(Features.TaprootZeroFeeCommitmentsStaging)
121+
).flatten
122+
override def commitmentFormat: CommitmentFormat = TaprootZeroFeeCommitmentFormat
123+
override def toString: String = s"taproot_zero_fee_commitments${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
124+
}
116125

117126
case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType {
118127
override def features: Set[InitFeature] = featureBits.activated.keySet
@@ -144,6 +153,10 @@ object ChannelTypes {
144153
ZeroFeeCommitments(zeroConf = true),
145154
ZeroFeeCommitments(scidAlias = true),
146155
ZeroFeeCommitments(scidAlias = true, zeroConf = true),
156+
TaprootZeroFeeCommitments(),
157+
TaprootZeroFeeCommitments(zeroConf = true),
158+
TaprootZeroFeeCommitments(scidAlias = true),
159+
TaprootZeroFeeCommitments(scidAlias = true, zeroConf = true),
147160
SimpleTaprootChannelsPhoenix,
148161
).map {
149162
channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType
@@ -163,7 +176,9 @@ object ChannelTypes {
163176

164177
/** Returns our preferred channel type for public channels, if supported by our peer. */
165178
def preferredForPublicChannels(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): Option[SupportedChannelType] = {
166-
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.ZeroFeeCommitments)) {
179+
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.TaprootZeroFeeCommitmentsStaging)) {
180+
Some(TaprootZeroFeeCommitments(scidAlias = !announceChannel && Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias)))
181+
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.ZeroFeeCommitments)) {
167182
Some(ZeroFeeCommitments(scidAlias = !announceChannel && Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias)))
168183
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) {
169184
Some(AnchorOutputsZeroFeeHtlcTx(scidAlias = !announceChannel && Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias)))

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ object LocalCommit {
173173
log.info(s"built local commit number=$localCommitIndex toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","))
174174
val remoteCommitSigOk = commitmentFormat match {
175175
case _: SegwitV0CommitmentFormat => localCommitTx.checkRemoteSig(fundingKey.publicKey, remoteFundingPubKey, commit.signature)
176-
case _: SimpleTaprootChannelCommitmentFormat => commit.sigOrPartialSig match {
176+
case _: TaprootCommitmentFormat => commit.sigOrPartialSig match {
177177
case _: IndividualSignature => false
178178
case remoteSig: PartialSignatureWithNonce =>
179179
val localNonce = NonceGenerator.verificationNonce(fundingTxId, fundingKey, remoteFundingPubKey, localCommitIndex)
@@ -211,7 +211,7 @@ case class RemoteCommit(index: Long, spec: CommitmentSpec, txId: TxId, remotePer
211211
case _: SegwitV0CommitmentFormat =>
212212
val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey)
213213
Right(CommitSig(channelParams.channelId, sig, htlcSigs.toList, batchSize))
214-
case _: SimpleTaprootChannelCommitmentFormat =>
214+
case _: TaprootCommitmentFormat =>
215215
remoteNonce_opt match {
216216
case Some(remoteNonce) =>
217217
val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey, remoteFundingPubKey, commitInput.outPoint.txid)
@@ -639,7 +639,7 @@ case class Commitment(fundingTxIndex: Long,
639639
Metrics.recordHtlcsInFlight(spec, remoteCommit.spec)
640640
val sig = commitmentFormat match {
641641
case _: SegwitV0CommitmentFormat => remoteCommitTx.sign(fundingKey, remoteFundingPubKey)
642-
case _: SimpleTaprootChannelCommitmentFormat =>
642+
case _: TaprootCommitmentFormat =>
643643
nextRemoteNonce_opt match {
644644
case Some(remoteNonce) =>
645645
val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey, remoteFundingPubKey, fundingTxId)
@@ -1102,7 +1102,7 @@ case class Commitments(channelParams: ChannelParams,
11021102
val localNextPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex + 2)
11031103
val localCommitNonces = active.flatMap(c => c.commitmentFormat match {
11041104
case _: SegwitV0CommitmentFormat => None
1105-
case _: SimpleTaprootChannelCommitmentFormat =>
1105+
case _: TaprootCommitmentFormat =>
11061106
val localNonce = NonceGenerator.verificationNonce(c.fundingTxId, c.localFundingKey(channelKeys), c.remoteFundingPubKey, localCommitIndex + 2)
11071107
Some(c.fundingTxId -> localNonce.publicNonce)
11081108
})

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

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ object Helpers {
133133
}
134134
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
135135
channelType.commitmentFormat match {
136-
case _: SimpleTaprootChannelCommitmentFormat => if (open.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0))
136+
case _: TaprootCommitmentFormat => if (open.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0))
137137
case _: SegwitV0CommitmentFormat => ()
138138
}
139139

@@ -243,7 +243,7 @@ object Helpers {
243243

244244
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
245245
channelType.commitmentFormat match {
246-
case _: SimpleTaprootChannelCommitmentFormat => if (accept.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0))
246+
case _: TaprootCommitmentFormat => if (accept.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0))
247247
case _: SegwitV0CommitmentFormat => ()
248248
}
249249
extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt))
@@ -295,7 +295,7 @@ object Helpers {
295295
channelType match {
296296
case _: ChannelTypes.AnchorOutputs | _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => remoteFeeratePerKw < FeeratePerKw.MinimumFeeratePerKw
297297
case _: ChannelTypes.SimpleTaprootChannelsStaging | ChannelTypes.SimpleTaprootChannelsPhoenix => remoteFeeratePerKw < FeeratePerKw.MinimumFeeratePerKw
298-
case _: ChannelTypes.ZeroFeeCommitments => false
298+
case _: ChannelTypes.ZeroFeeCommitments | _: ChannelTypes.TaprootZeroFeeCommitments => false
299299
}
300300
}
301301

@@ -558,7 +558,7 @@ object Helpers {
558558
val localNextPerCommitmentPoint = channelKeys.commitmentPoint(commitments.localCommitIndex + 1)
559559
val localCommitNonces = commitments.active.flatMap(c => c.commitmentFormat match {
560560
case _: SegwitV0CommitmentFormat => None
561-
case _: SimpleTaprootChannelCommitmentFormat =>
561+
case _: TaprootCommitmentFormat =>
562562
val fundingKey = channelKeys.fundingKey(c.fundingTxIndex)
563563
val n = NonceGenerator.verificationNonce(c.fundingTxId, fundingKey, c.remoteFundingPubKey, commitments.localCommitIndex + 1).publicNonce
564564
Some(c.fundingTxId -> n)
@@ -787,7 +787,7 @@ object Helpers {
787787
val dummySig = IndividualSignature(Transactions.PlaceHolderSig)
788788
val dummySignedTx = dummyTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig)
789789
SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.weight()))
790-
case _: SimpleTaprootChannelCommitmentFormat =>
790+
case _: TaprootCommitmentFormat =>
791791
val dummySignedTx = dummyTx.tx.updateWitness(dummyTx.inputIndex, Script.witnessKeyPathPay2tr(Transactions.PlaceHolderSig))
792792
SimpleClosingTxFee.PaidByUs(Transactions.weight2fee(feerate, dummySignedTx.weight()))
793793
}
@@ -803,7 +803,7 @@ object Helpers {
803803
val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
804804
val localNonces = CloserNonces.generate(localFundingKey.publicKey, commitment.remoteFundingPubKey, commitment.fundingTxId)
805805
val tlvs: TlvStream[ClosingCompleteTlv] = commitment.commitmentFormat match {
806-
case _: SimpleTaprootChannelCommitmentFormat =>
806+
case _: TaprootCommitmentFormat =>
807807
remoteNonce_opt match {
808808
case None => return Left(MissingClosingNonce(commitment.channelId))
809809
case Some(remoteNonce) =>
@@ -842,7 +842,7 @@ object Helpers {
842842
// If our output isn't dust, they must provide a signature for a transaction that includes it.
843843
// Note that we're the closee, so we look for signatures including the closee output.
844844
commitment.commitmentFormat match {
845-
case _: SimpleTaprootChannelCommitmentFormat => localNonce_opt match {
845+
case _: TaprootCommitmentFormat => localNonce_opt match {
846846
case None => Left(MissingClosingNonce(commitment.channelId))
847847
case Some(localNonce) =>
848848
(closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
@@ -1207,7 +1207,7 @@ object Helpers {
12071207
// In that case, we don't need to create a dedicated anchor transaction which avoids using wallet inputs.
12081208
val useMainTxForAnchor = commitment.commitmentFormat match {
12091209
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => false
1210-
case ZeroFeeCommitmentFormat =>
1210+
case ZeroFeeCommitmentFormat | TaprootZeroFeeCommitmentFormat =>
12111211
val commitFee = Transactions.weight2fee(feerates.fastest, commitTx.weight())
12121212
mainTx_opt.exists(_.tx.txOut.map(_.amount).sum > commitFee)
12131213
}
@@ -1237,10 +1237,8 @@ object Helpers {
12371237

12381238
/** Claim our main output from the remote commitment transaction, if available. */
12391239
def claimMainOutput(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, dustLimit: Satoshi, commitmentFormat: CommitmentFormat, feerate: FeeratePerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Option[ClaimRemoteMainOutputTx] = {
1240-
commitmentFormat match {
1241-
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat | ZeroFeeCommitmentFormat => withTxGenerationLog("remote-main") {
1242-
ClaimRemoteMainOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerate, commitmentFormat)
1243-
}
1240+
withTxGenerationLog("remote-main") {
1241+
ClaimRemoteMainOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerate, commitmentFormat)
12441242
}
12451243
}
12461244

@@ -1405,10 +1403,8 @@ object Helpers {
14051403
val feeratePenalty = feerates.fast
14061404

14071405
// First we will claim our main output right away.
1408-
val mainTx_opt = commitmentFormat match {
1409-
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat | ZeroFeeCommitmentFormat => withTxGenerationLog("remote-main") {
1410-
ClaimRemoteMainOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerateMain, commitmentFormat)
1411-
}
1406+
val mainTx_opt = withTxGenerationLog("remote-main") {
1407+
ClaimRemoteMainOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerateMain, commitmentFormat)
14121408
}
14131409

14141410
// Then we punish them by stealing their main output.

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ object Channel {
157157
case _: ChannelTypes.AnchorOutputs | _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => 483
158158
case _: ChannelTypes.SimpleTaprootChannelsStaging | ChannelTypes.SimpleTaprootChannelsPhoenix => 483
159159
// When using v3 transactions, the maximum package size is more restrictive than v2 transactions.
160-
case _: ChannelTypes.ZeroFeeCommitments => 114
160+
case _: ChannelTypes.ZeroFeeCommitments | _: ChannelTypes.TaprootZeroFeeCommitments => 114
161161
}
162162

163163
// We may need to rely on our peer's commit tx in certain cases (backup/restore) so we must ensure their transactions
@@ -2422,7 +2422,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
24222422
val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTxId))
24232423
val nonceTlvs = d.signingSession.fundingParams.commitmentFormat match {
24242424
case _: SegwitV0CommitmentFormat => Set.empty
2425-
case _: SimpleTaprootChannelCommitmentFormat =>
2425+
case _: TaprootCommitmentFormat =>
24262426
val localFundingKey = channelKeys.fundingKey(0)
24272427
val remoteFundingPubKey = d.signingSession.fundingParams.remoteFundingPubKey
24282428
val currentCommitNonce_opt = d.signingSession.localCommit match {
@@ -2490,7 +2490,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
24902490
val nextCommitNonces: Map[TxId, IndividualNonce] = d.commitments.active.flatMap(c => {
24912491
c.commitmentFormat match {
24922492
case _: SegwitV0CommitmentFormat => None
2493-
case _: SimpleTaprootChannelCommitmentFormat =>
2493+
case _: TaprootCommitmentFormat =>
24942494
val localFundingKey = channelKeys.fundingKey(c.fundingTxIndex)
24952495
Some(c.fundingTxId -> NonceGenerator.verificationNonce(c.fundingTxId, localFundingKey, c.remoteFundingPubKey, d.commitments.localCommitIndex + 1).publicNonce)
24962496
}

0 commit comments

Comments
 (0)