Skip to content

Commit 33cd067

Browse files
committed
Make channel type features non-permanent
We want to be able to update channel types during splices, which means that channel type features will not be permanent anymore. We decouple permanent channel features (that apply to the lifetime of the channel) from channel type features to allow this.
1 parent b78ed5a commit 33cd067

File tree

28 files changed

+215
-219
lines changed

28 files changed

+215
-219
lines changed

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,15 @@ object FeatureSupport {
3636

3737
/** Not a sealed trait, so it can be extended by plugins. */
3838
trait Feature {
39-
4039
def rfcName: String
4140
def mandatory: Int
4241
def optional: Int = mandatory + 1
43-
4442
def supportBit(support: FeatureSupport): Int = support match {
4543
case Mandatory => mandatory
4644
case Optional => optional
4745
}
4846

49-
override def toString = rfcName
47+
override def toString: String = rfcName
5048
}
5149

5250
/** Feature scope as defined in Bolt 9. */
@@ -68,11 +66,9 @@ trait Bolt12Feature extends InvoiceFeature
6866
*/
6967
trait PermanentChannelFeature extends InitFeature // <- not in the spec
7068
/**
71-
* Permanent channel feature negotiated in the channel type. Those features take precedence over permanent channel
72-
* features negotiated in init messages. For example, if the channel type is option_static_remotekey, then even if
73-
* the option_anchor_outputs feature is supported by both peers, it won't apply to the channel.
69+
* Features that can be included in the [[fr.acinq.eclair.wire.protocol.ChannelTlv.ChannelTypeTlv]].
7470
*/
75-
trait ChannelTypeFeature extends PermanentChannelFeature
71+
trait ChannelTypeFeature extends InitFeature
7672
// @formatter:on
7773

7874
case class UnknownFeature(bitIndex: Int)
@@ -275,6 +271,7 @@ object Features {
275271
val rfcName = "option_attribution_data"
276272
val mandatory = 36
277273
}
274+
278275
case object OnionMessages extends Feature with InitFeature with NodeFeature {
279276
val rfcName = "option_onion_messages"
280277
val mandatory = 38
@@ -290,7 +287,7 @@ object Features {
290287
val mandatory = 44
291288
}
292289

293-
case object ScidAlias extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
290+
case object ScidAlias extends Feature with InitFeature with NodeFeature with ChannelTypeFeature with PermanentChannelFeature {
294291
val rfcName = "option_scid_alias"
295292
val mandatory = 46
296293
}
@@ -300,7 +297,7 @@ object Features {
300297
val mandatory = 48
301298
}
302299

303-
case object ZeroConf extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
300+
case object ZeroConf extends Feature with InitFeature with NodeFeature with ChannelTypeFeature with PermanentChannelFeature {
304301
val rfcName = "option_zeroconf"
305302
val mandatory = 50
306303
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,7 @@ final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INI
557557
val channelId: ByteVector32 = initFunder.temporaryChannelId
558558
}
559559
final case class DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams: ChannelParams,
560+
channelType: SupportedChannelType,
560561
localCommitParams: CommitParams,
561562
remoteCommitParams: CommitParams,
562563
fundingAmount: Satoshi,
@@ -566,9 +567,10 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams: ChannelParams,
566567
remoteFirstPerCommitmentPoint: PublicKey,
567568
replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData {
568569
val channelId: ByteVector32 = channelParams.channelId
569-
val commitmentFormat: CommitmentFormat = channelParams.channelFeatures.commitmentFormat
570+
val commitmentFormat: CommitmentFormat = channelType.commitmentFormat
570571
}
571572
final case class DATA_WAIT_FOR_FUNDING_CREATED(channelParams: ChannelParams,
573+
channelType: SupportedChannelType,
572574
localCommitParams: CommitParams,
573575
remoteCommitParams: CommitParams,
574576
fundingAmount: Satoshi,
@@ -577,9 +579,10 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(channelParams: ChannelParams,
577579
remoteFundingPubKey: PublicKey,
578580
remoteFirstPerCommitmentPoint: PublicKey) extends TransientChannelData {
579581
val channelId: ByteVector32 = channelParams.channelId
580-
val commitmentFormat: CommitmentFormat = channelParams.channelFeatures.commitmentFormat
582+
val commitmentFormat: CommitmentFormat = channelType.commitmentFormat
581583
}
582584
final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelParams: ChannelParams,
585+
channelType: SupportedChannelType,
583586
localCommitParams: CommitParams,
584587
remoteCommitParams: CommitParams,
585588
remoteFundingPubKey: PublicKey,
@@ -591,7 +594,7 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelParams: ChannelParams,
591594
lastSent: FundingCreated,
592595
replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData {
593596
val channelId: ByteVector32 = channelParams.channelId
594-
val commitmentFormat: CommitmentFormat = channelParams.channelFeatures.commitmentFormat
597+
val commitmentFormat: CommitmentFormat = channelType.commitmentFormat
595598
}
596599
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments,
597600
waitingSince: BlockHeight, // how long have we been waiting for the funding tx to confirm

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

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,10 @@ import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeatur
2525

2626
/**
2727
* Subset of Bolt 9 features used to configure a channel and applicable over the lifetime of that channel.
28-
* Even if one of these features is later disabled at the connection level, it will still apply to the channel until the
29-
* channel is upgraded or closed.
28+
* Even if one of these features is later disabled at the connection level, it will still apply to the channel.
3029
*/
3130
case class ChannelFeatures(features: Set[PermanentChannelFeature]) {
3231

33-
/** 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)
35-
/** 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)) {
37-
UnsafeLegacyAnchorOutputsCommitmentFormat
38-
} else if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) {
39-
ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
40-
} else {
41-
DefaultCommitmentFormat
42-
}
43-
4432
def hasFeature(feature: PermanentChannelFeature): Boolean = features.contains(feature)
4533

4634
override def toString: String = features.mkString(",")
@@ -51,19 +39,24 @@ object ChannelFeatures {
5139

5240
def apply(features: PermanentChannelFeature*): ChannelFeatures = ChannelFeatures(Set.from(features))
5341

54-
/** Enrich the channel type with other permanent features that will be applied to the channel. */
42+
/** Configure permanent channel features based on local and remote feature. */
5543
def apply(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): ChannelFeatures = {
56-
val additionalPermanentFeatures = Features.knownFeatures.collect {
44+
val permanentFeatures = Features.knownFeatures.collect {
5745
// If we both support 0-conf or scid_alias, we use it even if it wasn't in the channel-type.
58-
case Features.ScidAlias if Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias) && !announceChannel => Some(Features.ScidAlias)
46+
// Note that we cannot use scid_alias if the channel is announced.
47+
case Features.ScidAlias =>
48+
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias) && !announceChannel) {
49+
Some(Features.ScidAlias)
50+
} else {
51+
None
52+
}
5953
case Features.ZeroConf if Features.canUseFeature(localFeatures, remoteFeatures, Features.ZeroConf) => Some(Features.ZeroConf)
60-
// Other channel-type features are negotiated in the channel-type, we ignore their value from the init message.
61-
case _: ChannelTypeFeature => None
62-
// We add all other permanent channel features that aren't negotiated as part of the channel-type.
54+
// We add all other permanent channel features that we both support.
6355
case f: PermanentChannelFeature if Features.canUseFeature(localFeatures, remoteFeatures, f) => Some(f)
6456
}.flatten
65-
val allPermanentFeatures = channelType.features.toSeq ++ additionalPermanentFeatures
66-
ChannelFeatures(allPermanentFeatures: _*)
57+
// Some permanent features can be negotiated as part of the channel-type.
58+
val channelTypeFeatures = channelType.features.collect { case f: PermanentChannelFeature => f }
59+
ChannelFeatures(permanentFeatures ++ channelTypeFeatures)
6760
}
6861

6962
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ case class ChannelParams(channelId: ByteVector32,
2525
channelFeatures: ChannelFeatures,
2626
localParams: LocalChannelParams, remoteParams: RemoteChannelParams,
2727
channelFlags: ChannelFlags) {
28-
require(channelFeatures.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (channel features: $channelFeatures")
2928
require(channelFeatures.hasFeature(Features.DualFunding) == localParams.initialRequestedChannelReserve_opt.isEmpty, "custom local channel reserve is incompatible with dual-funded channels")
3029
require(channelFeatures.hasFeature(Features.DualFunding) == remoteParams.initialRequestedChannelReserve_opt.isEmpty, "custom remote channel reserve is incompatible with dual-funded channels")
3130

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ object Helpers {
133133
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
134134

135135
// BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large.
136-
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingSatoshis)
137-
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures.commitmentFormat, localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw))
136+
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat, open.fundingSatoshis)
137+
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelType.commitmentFormat, localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw))
138138

139139
// we don't check that the funder's amount for the initial commitment transaction is sufficient for full fee payment
140140
// now, but it will be done later when we receive `funding_created`
@@ -180,8 +180,8 @@ object Helpers {
180180
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)
181181

182182
// BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large.
183-
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingAmount)
184-
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures.commitmentFormat, localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate))
183+
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat, open.fundingAmount)
184+
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelType.commitmentFormat, localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate))
185185

186186
for {
187187
script_opt <- extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
142142
ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript),
143143
ChannelTlv.ChannelTypeTlv(d.initFundee.channelType)
144144
))
145-
goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(channelParams, localCommitParams, remoteCommitParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.fundingPubkey, open.firstPerCommitmentPoint) sending accept
145+
goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(channelParams, d.initFundee.channelType, localCommitParams, remoteCommitParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.fundingPubkey, open.firstPerCommitmentPoint) sending accept
146146
}
147147

148148
case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId)
@@ -175,7 +175,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
175175
val channelParams = ChannelParams(d.initFunder.temporaryChannelId, d.initFunder.channelConfig, channelFeatures, d.initFunder.localChannelParams, remoteChannelParams, d.lastSent.channelFlags)
176176
val localCommitParams = CommitParams(d.initFunder.proposedCommitParams.localDustLimit, d.initFunder.proposedCommitParams.localHtlcMinimum, d.initFunder.proposedCommitParams.localMaxHtlcValueInFlight, d.initFunder.proposedCommitParams.localMaxAcceptedHtlcs, accept.toSelfDelay)
177177
val remoteCommitParams = CommitParams(accept.dustLimitSatoshis, accept.htlcMinimumMsat, accept.maxHtlcValueInFlightMsat, accept.maxAcceptedHtlcs, d.initFunder.proposedCommitParams.toRemoteDelay)
178-
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams, localCommitParams, remoteCommitParams, d.initFunder.fundingAmount, d.initFunder.pushAmount_opt.getOrElse(0 msat), d.initFunder.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo)
178+
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(channelParams, d.initFunder.channelType, localCommitParams, remoteCommitParams, d.initFunder.fundingAmount, d.initFunder.pushAmount_opt.getOrElse(0 msat), d.initFunder.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo)
179179
}
180180

181181
case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) =>
@@ -224,7 +224,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
224224
txPublisher ! SetChannelId(remoteNodeId, channelId)
225225
context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId))
226226
// NB: we don't send a ChannelSignatureSent for the first commit
227-
goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelParams1, d.localCommitParams, d.remoteCommitParams, d.remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, d.replyTo) sending fundingCreated
227+
goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelParams1, d.channelType, d.localCommitParams, d.remoteCommitParams, d.remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, d.replyTo) sending fundingCreated
228228
}
229229

230230
case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) =>

eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/CommitmentKeys.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ object LocalCommitmentKeys {
7272
LocalCommitmentKeys(
7373
ourDelayedPaymentKey = channelKeys.delayedPaymentKey(localPerCommitmentPoint),
7474
theirPaymentPublicKey = commitmentFormat match {
75-
case DefaultCommitmentFormat if params.channelFeatures.hasFeature(Features.StaticRemoteKey) => params.remoteParams.paymentBasepoint
75+
case DefaultCommitmentFormat if params.localParams.walletStaticPaymentBasepoint.nonEmpty => params.remoteParams.paymentBasepoint
7676
case DefaultCommitmentFormat => ChannelKeys.remotePerCommitmentPublicKey(params.remoteParams.paymentBasepoint, localPerCommitmentPoint)
7777
case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => params.remoteParams.paymentBasepoint
7878
},

eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,10 @@ private[channel] object ChannelTypes0 {
230230
} else {
231231
ChannelConfig()
232232
}
233-
val channelFeatures = if (channelVersion.hasAnchorOutputs) {
234-
ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)
235-
} else if (channelVersion.hasStaticRemotekey) {
236-
ChannelFeatures(Features.StaticRemoteKey)
233+
val commitmentFormat = if (channelVersion.hasAnchorOutputs) {
234+
UnsafeLegacyAnchorOutputsCommitmentFormat
237235
} else {
238-
ChannelFeatures()
236+
DefaultCommitmentFormat
239237
}
240238
val (localCommit1, commitInput) = localCommit.migrate(remoteParams.fundingPubKey)
241239
val localCommitParams = CommitParams(localParams.dustLimit, localParams.htlcMinimum, localParams.maxHtlcValueInFlightMsat, localParams.maxAcceptedHtlcs, remoteParams.toRemoteDelay)
@@ -251,15 +249,15 @@ private[channel] object ChannelTypes0 {
251249
// funding tx when the channel is instantiated, and update the status (possibly immediately if it was confirmed).
252250
localFundingStatus = LocalFundingStatus.SingleFundedUnconfirmedFundingTx(None),
253251
remoteFundingStatus = RemoteFundingStatus.Locked,
254-
commitmentFormat = channelFeatures.commitmentFormat,
252+
commitmentFormat = commitmentFormat,
255253
localCommitParams = localCommitParams,
256254
localCommit = localCommit1,
257255
remoteCommitParams = remoteCommitParams,
258256
remoteCommit = remoteCommit,
259257
nextRemoteCommit_opt = remoteNextCommitInfo.left.toOption.map(w => NextRemoteCommit(w.sent, w.nextRemoteCommit))
260258
)
261259
channel.Commitments(
262-
ChannelParams(channelId, channelConfig, channelFeatures, localParams.migrate(), remoteParams.migrate(), channelFlags),
260+
ChannelParams(channelId, channelConfig, ChannelFeatures(), localParams.migrate(), remoteParams.migrate(), channelFlags),
263261
CommitmentChanges(localChanges, remoteChanges, localNextHtlcId, remoteNextHtlcId),
264262
Seq(commitment),
265263
inactive = Nil,

0 commit comments

Comments
 (0)