Skip to content

Commit 8b2e2e3

Browse files
committed
Add support for zero-fee commitment format
We add support for the zero-fee commitment format specified in lightning/bolts#1228. 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. In this commit, we simply introduce the commitment format and create the corresponding transactions.
1 parent df75ed5 commit 8b2e2e3

File tree

16 files changed

+503
-60
lines changed

16 files changed

+503
-60
lines changed

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

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

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,12 @@ object Features {
277277
val mandatory = 38
278278
}
279279

280+
// TODO: once the spec is final, set feature bit to 40 and activate in reference.conf
281+
case object ZeroFeeCommitments extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
282+
val rfcName = "zero_fee_commitments"
283+
val mandatory = 140
284+
}
285+
280286
case object ProvideStorage extends Feature with InitFeature with NodeFeature {
281287
val rfcName = "option_provide_storage"
282288
val mandatory = 42
@@ -392,6 +398,7 @@ object Features {
392398
Quiescence,
393399
AttributionData,
394400
OnionMessages,
401+
ZeroFeeCommitments,
395402
ProvideStorage,
396403
ChannelType,
397404
ScidAlias,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ 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 =>
97+
// We never send update_fee when using zero-fee commitments.
98+
false
9699
}
97100
}
98101

@@ -111,6 +114,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
111114
val networkFeerate = feerates.fast
112115
val networkMinFee = feerates.minimum
113116
commitmentFormat match {
117+
case Transactions.ZeroFeeCommitmentFormat => FeeratePerKw(0 sat)
114118
case Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat | Transactions.PhoenixSimpleTaprootChannelCommitmentFormat =>
115119
// Since Bitcoin Core v28, 1-parent-1-child package relay has been deployed: it should be ok if the commit tx
116120
// doesn't propagate on its own.

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ object ChannelTypes {
9595
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
9696
override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
9797
}
98+
case class ZeroFeeCommitments(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType {
99+
override def features: Set[ChannelTypeFeature] = Set(
100+
if (scidAlias) Some(Features.ScidAlias) else None,
101+
if (zeroConf) Some(Features.ZeroConf) else None,
102+
Some(Features.ZeroFeeCommitments)
103+
).flatten
104+
override def commitmentFormat: CommitmentFormat = ZeroFeeCommitmentFormat
105+
override def toString: String = s"zero_fee_commitments${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}"
106+
}
98107
case class SimpleTaprootChannelsStaging(scidAlias: Boolean = false, zeroConf: Boolean = false) extends SupportedChannelType {
99108
override def features: Set[ChannelTypeFeature] = Set(
100109
if (scidAlias) Some(Features.ScidAlias) else None,
@@ -132,6 +141,10 @@ object ChannelTypes {
132141
SimpleTaprootChannelsStaging(zeroConf = true),
133142
SimpleTaprootChannelsStaging(scidAlias = true),
134143
SimpleTaprootChannelsStaging(scidAlias = true, zeroConf = true),
144+
ZeroFeeCommitments(),
145+
ZeroFeeCommitments(zeroConf = true),
146+
ZeroFeeCommitments(scidAlias = true),
147+
ZeroFeeCommitments(scidAlias = true, zeroConf = true),
135148
SimpleTaprootChannelsPhoenix,
136149
).map {
137150
channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ object Commitment {
725725
commitmentFormat: CommitmentFormat,
726726
spec: CommitmentSpec): (CommitTx, Seq[UnsignedHtlcTx]) = {
727727
val outputs = makeCommitTxOutputs(localFundingKey.publicKey, remoteFundingPubKey, commitKeys.publicKeys, channelParams.localParams.paysCommitTxFees, commitParams.dustLimit, commitParams.toSelfDelay, spec, commitmentFormat)
728-
val commitTx = makeCommitTx(commitmentInput, commitTxNumber, commitKeys.ourPaymentBasePoint, channelParams.remoteParams.paymentBasepoint, channelParams.localParams.isChannelOpener, outputs)
728+
val commitTx = makeCommitTx(commitmentInput, commitTxNumber, commitKeys.ourPaymentBasePoint, channelParams.remoteParams.paymentBasepoint, channelParams.localParams.isChannelOpener, commitmentFormat, outputs)
729729
val htlcTxs = makeHtlcTxs(commitTx.tx, outputs, commitmentFormat)
730730
(commitTx, htlcTxs)
731731
}
@@ -740,7 +740,7 @@ object Commitment {
740740
commitmentFormat: CommitmentFormat,
741741
spec: CommitmentSpec): (CommitTx, Seq[UnsignedHtlcTx]) = {
742742
val outputs = makeCommitTxOutputs(remoteFundingPubKey, localFundingKey.publicKey, commitKeys.publicKeys, !channelParams.localParams.paysCommitTxFees, commitParams.dustLimit, commitParams.toSelfDelay, spec, commitmentFormat)
743-
val commitTx = makeCommitTx(commitmentInput, commitTxNumber, channelParams.remoteParams.paymentBasepoint, commitKeys.ourPaymentBasePoint, !channelParams.localParams.isChannelOpener, outputs)
743+
val commitTx = makeCommitTx(commitmentInput, commitTxNumber, channelParams.remoteParams.paymentBasepoint, commitKeys.ourPaymentBasePoint, !channelParams.localParams.isChannelOpener, commitmentFormat, outputs)
744744
val htlcTxs = makeHtlcTxs(commitTx.tx, outputs, commitmentFormat)
745745
(commitTx, htlcTxs)
746746
}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ object Helpers {
140140
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
141141
channelType.commitmentFormat match {
142142
case _: SimpleTaprootChannelCommitmentFormat => if (open.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0))
143-
case _: AnchorOutputsCommitmentFormat => ()
143+
case _: SegwitV0CommitmentFormat => ()
144144
}
145145

146146
// BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large.
@@ -244,7 +244,7 @@ object Helpers {
244244
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
245245
channelType.commitmentFormat match {
246246
case _: SimpleTaprootChannelCommitmentFormat => if (accept.commitNonce_opt.isEmpty) return Left(MissingCommitNonce(open.temporaryChannelId, TxId(ByteVector32.Zeroes), commitmentNumber = 0))
247-
case _: AnchorOutputsCommitmentFormat => ()
247+
case _: SegwitV0CommitmentFormat => ()
248248
}
249249
extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt))
250250
}
@@ -762,7 +762,7 @@ object Helpers {
762762
dummyClosingTxs.preferred_opt match {
763763
case Some(dummyTx) =>
764764
commitment.commitmentFormat match {
765-
case _: AnchorOutputsCommitmentFormat =>
765+
case _: SegwitV0CommitmentFormat =>
766766
val dummyPubkey = commitment.remoteFundingPubKey
767767
val dummySig = IndividualSignature(Transactions.PlaceHolderSig)
768768
val dummySignedTx = dummyTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig)
@@ -800,7 +800,7 @@ object Helpers {
800800
closingTxs.remoteOnly_opt.flatMap(tx => localSig(tx, localNonces.remoteOnly)).map(ClosingCompleteTlv.CloseeOutputOnlyPartialSignature(_)),
801801
).flatten[ClosingCompleteTlv])
802802
}
803-
case _: AnchorOutputsCommitmentFormat => TlvStream(Set(
803+
case _: SegwitV0CommitmentFormat => TlvStream(Set(
804804
closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
805805
closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
806806
closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(tx.sign(localFundingKey, commitment.remoteFundingPubKey).sig)),
@@ -854,7 +854,7 @@ object Helpers {
854854
case None => Left(MissingCloseSignature(commitment.channelId))
855855
}
856856
}
857-
case _: AnchorOutputsCommitmentFormat =>
857+
case _: SegwitV0CommitmentFormat =>
858858
(closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match {
859859
case (Some(_), Some(_)) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty && closingComplete.closeeOutputOnlySig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
860860
case (Some(_), None) if closingComplete.closerAndCloseeOutputsSig_opt.isEmpty => return Left(MissingCloseSignature(commitment.channelId))
@@ -1209,7 +1209,7 @@ object Helpers {
12091209
/** Claim our main output from the remote commitment transaction, if available. */
12101210
def claimMainOutput(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, dustLimit: Satoshi, commitmentFormat: CommitmentFormat, feerate: FeeratePerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Option[ClaimRemoteDelayedOutputTx] = {
12111211
commitmentFormat match {
1212-
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") {
1212+
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat | ZeroFeeCommitmentFormat => withTxGenerationLog("remote-main-delayed") {
12131213
ClaimRemoteDelayedOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerate, commitmentFormat)
12141214
}
12151215
}
@@ -1377,7 +1377,7 @@ object Helpers {
13771377

13781378
// First we will claim our main output right away.
13791379
val mainTx_opt = commitmentFormat match {
1380-
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => withTxGenerationLog("remote-main-delayed") {
1380+
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat | ZeroFeeCommitmentFormat => withTxGenerationLog("remote-main-delayed") {
13811381
ClaimRemoteDelayedOutputTx.createUnsignedTx(commitKeys, commitTx, dustLimit, finalScriptPubKey, feerateMain, commitmentFormat)
13821382
}
13831383
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentK
3232
import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain}
3333
import fr.acinq.eclair.io.Peer.OpenChannelResponse
3434
import fr.acinq.eclair.transactions.Transactions
35-
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
35+
import fr.acinq.eclair.transactions.Transactions.{SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat}
3636
import fr.acinq.eclair.wire.protocol.{AcceptChannel, AcceptChannelTlv, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, OpenChannelTlv, TlvStream}
3737
import fr.acinq.eclair.{MilliSatoshiLong, randomKey, toLongId}
3838
import scodec.bits.ByteVector
@@ -79,7 +79,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
7979
val localShutdownScript = input.localChannelParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty)
8080
val localNonce = input.channelType.commitmentFormat match {
8181
case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0).publicNonce)
82-
case _: AnchorOutputsCommitmentFormat => None
82+
case _: SegwitV0CommitmentFormat => None
8383
}
8484
val open = OpenChannel(
8585
chainHash = nodeParams.chainHash,
@@ -134,7 +134,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
134134
val localShutdownScript = d.initFundee.localChannelParams.upfrontShutdownScript_opt.getOrElse(ByteVector.empty)
135135
val localNonce = d.initFundee.channelType.commitmentFormat match {
136136
case _: SimpleTaprootChannelCommitmentFormat => Some(NonceGenerator.verificationNonce(NonceGenerator.dummyFundingTxId, fundingKey, NonceGenerator.dummyRemoteFundingPubKey, 0).publicNonce)
137-
case _: AnchorOutputsCommitmentFormat => None
137+
case _: SegwitV0CommitmentFormat => None
138138
}
139139
val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId,
140140
dustLimitSatoshis = localCommitParams.dustLimit,
@@ -233,7 +233,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
233233
}
234234
case None => Left(MissingCommitNonce(d.channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0))
235235
}
236-
case _: AnchorOutputsCommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey))
236+
case _: SegwitV0CommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey))
237237
}
238238
localSigOfRemoteTx match {
239239
case Left(f) => handleLocalError(f, d, None)
@@ -303,7 +303,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
303303
}
304304
case None => Left(MissingCommitNonce(channelId, NonceGenerator.dummyFundingTxId, commitmentNumber = 0))
305305
}
306-
case _: AnchorOutputsCommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey))
306+
case _: SegwitV0CommitmentFormat => Right(remoteCommitTx.sign(fundingKey, d.remoteFundingPubKey))
307307
}
308308
localSigOfRemoteTx match {
309309
case Left(f) => handleLocalError(f, d, Some(fc))

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import fr.acinq.eclair.channel._
2626
import fr.acinq.eclair.channel.fsm.Channel.{BroadcastChannelUpdate, PeriodicRefresh, REFRESH_CHANNEL_UPDATE_INTERVAL}
2727
import fr.acinq.eclair.crypto.NonceGenerator
2828
import fr.acinq.eclair.db.RevokedHtlcInfoCleaner
29-
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
29+
import fr.acinq.eclair.transactions.Transactions.{SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat}
3030
import fr.acinq.eclair.wire.protocol._
3131
import fr.acinq.eclair.{RealShortChannelId, ShortChannelId}
3232

@@ -132,7 +132,7 @@ trait CommonFundingHandlers extends CommonHandlers {
132132
val localFundingKey = channelKeys.fundingKey(fundingTxIndex = 0)
133133
val nextLocalNonce = NonceGenerator.verificationNonce(commitments.latest.fundingTxId, localFundingKey, commitments.latest.remoteFundingPubKey, 1)
134134
ChannelReady(params.channelId, nextPerCommitmentPoint, aliases.localAlias, nextLocalNonce.publicNonce)
135-
case _: AnchorOutputsCommitmentFormat =>
135+
case _: SegwitV0CommitmentFormat =>
136136
ChannelReady(params.channelId, nextPerCommitmentPoint, aliases.localAlias)
137137
}
138138
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import fr.acinq.eclair.channel._
2424
import fr.acinq.eclair.crypto.NonceGenerator
2525
import fr.acinq.eclair.db.PendingCommandsDb
2626
import fr.acinq.eclair.io.Peer
27-
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, SimpleTaprootChannelCommitmentFormat}
27+
import fr.acinq.eclair.transactions.Transactions.{SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat}
2828
import fr.acinq.eclair.wire.protocol.{ClosingComplete, HtlcSettlementMessage, LightningMessage, Shutdown, UpdateMessage}
2929
import scodec.bits.ByteVector
3030

@@ -142,7 +142,7 @@ trait CommonHandlers {
142142
val localCloseeNonce = NonceGenerator.signingNonce(localFundingPubKey, commitments.latest.remoteFundingPubKey, commitments.latest.fundingTxId)
143143
localCloseeNonce_opt = Some(localCloseeNonce)
144144
Shutdown(commitments.channelId, finalScriptPubKey, localCloseeNonce.publicNonce)
145-
case _: AnchorOutputsCommitmentFormat =>
145+
case _: SegwitV0CommitmentFormat =>
146146
Shutdown(commitments.channelId, finalScriptPubKey)
147147
}
148148
}

eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ object CommitmentOutput {
3636
case class ToRemote(txOut: TxOut) extends CommitmentOutput
3737
case class ToLocalAnchor(txOut: TxOut) extends CommitmentOutput
3838
case class ToRemoteAnchor(txOut: TxOut) extends CommitmentOutput
39+
case class ToSharedAnchor(txOut: TxOut) extends CommitmentOutput
3940
// If there is an output for an HTLC in the commit tx, there is also a 2nd-level HTLC tx.
4041
case class InHtlc(htlc: IncomingHtlc, txOut: TxOut, htlcDelayedOutput: TxOut) extends CommitmentOutput
4142
case class OutHtlc(htlc: OutgoingHtlc, txOut: TxOut, htlcDelayedOutput: TxOut) extends CommitmentOutput
@@ -94,6 +95,7 @@ final case class CommitmentSpec(htlcs: Set[DirectedHtlc], commitTxFeerate: Feera
9495

9596
def htlcTxFeerate(commitmentFormat: CommitmentFormat): FeeratePerKw = commitmentFormat match {
9697
case ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat => FeeratePerKw(0 sat)
98+
case ZeroFeeCommitmentFormat => FeeratePerKw(0 sat)
9799
case UnsafeLegacyAnchorOutputsCommitmentFormat | PhoenixSimpleTaprootChannelCommitmentFormat => commitTxFeerate
98100
}
99101

0 commit comments

Comments
 (0)