Skip to content

Commit 0d78dc1

Browse files
committed
Separate internal channel config from features
Our current ChannelVersion field mixes two unrelated concepts: channel features (as defined in Bolt 9) and channel internals (such as custom key derivation). It is more future-proof to separate these two unrelated concepts and will make it easier to implement channel types (see lightning/bolts#880).
1 parent 2e074f7 commit 0d78dc1

Some content is hidden

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

44 files changed

+819
-479
lines changed

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

+8-7
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ package fr.acinq.eclair.blockchain.fee
1818

1919
import fr.acinq.bitcoin.Crypto.PublicKey
2020
import fr.acinq.bitcoin.Satoshi
21+
import fr.acinq.eclair.Features
2122
import fr.acinq.eclair.blockchain.CurrentFeerates
22-
import fr.acinq.eclair.channel.ChannelVersion
23+
import fr.acinq.eclair.channel.ChannelFeatures
2324

2425
trait FeeEstimator {
2526
// @formatter:off
@@ -32,13 +33,13 @@ case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutua
3233

3334
case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMaxCommitFeerate: FeeratePerKw) {
3435
/**
35-
* @param channelVersion channel version
36+
* @param channelFeatures permanent channel features
3637
* @param networkFeerate reference fee rate (value we estimate from our view of the network)
3738
* @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx)
3839
* @return true if the difference between proposed and reference fee rates is too high.
3940
*/
40-
def isFeeDiffTooHigh(channelVersion: ChannelVersion, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
41-
if (channelVersion.hasAnchorOutputs) {
41+
def isFeeDiffTooHigh(channelFeatures: ChannelFeatures, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
42+
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
4243
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
4344
} else {
4445
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
@@ -60,15 +61,15 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl
6061
* - otherwise we use a feerate that should get the commit tx confirmed within the configured block target
6162
*
6263
* @param remoteNodeId nodeId of our channel peer
63-
* @param channelVersion channel version
64+
* @param channelFeatures permanent channel features
6465
* @param currentFeerates_opt if provided, will be used to compute the most up-to-date network fee, otherwise we rely on the fee estimator
6566
*/
66-
def getCommitmentFeerate(remoteNodeId: PublicKey, channelVersion: ChannelVersion, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
67+
def getCommitmentFeerate(remoteNodeId: PublicKey, channelFeatures: ChannelFeatures, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
6768
val networkFeerate = currentFeerates_opt match {
6869
case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget)
6970
case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget)
7071
}
71-
if (channelVersion.hasAnchorOutputs) {
72+
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
7273
networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
7374
} else {
7475
networkFeerate

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

+33-33
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2021 ACINQ SAS
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package fr.acinq.eclair.channel
18+
19+
/**
20+
* Created by t-bast on 24/06/2021.
21+
*/
22+
23+
/**
24+
* Internal configuration option impacting the channel's structure or behavior.
25+
* This must be set when creating the channel and cannot be changed afterwards.
26+
*/
27+
trait ChannelConfigOption {
28+
// @formatter:off
29+
def supportBit: Int
30+
def name: String
31+
// @formatter:on
32+
}
33+
34+
case class ChannelConfigOptions(activated: Set[ChannelConfigOption]) {
35+
36+
def hasOption(option: ChannelConfigOption): Boolean = activated.contains(option)
37+
38+
}
39+
40+
object ChannelConfigOptions {
41+
42+
def standard: ChannelConfigOptions = ChannelConfigOptions(activated = Set(FundingPubKeyBasedChannelKeyPath))
43+
44+
def apply(options: ChannelConfigOption*): ChannelConfigOptions = ChannelConfigOptions(Set.from(options))
45+
46+
/**
47+
* If set, the channel's BIP32 key path will be deterministically derived from the funding public key.
48+
* It makes it very easy to retrieve funds when channel data has been lost:
49+
* - connect to your peer and use option_data_loss_protect to get them to publish their remote commit tx
50+
* - retrieve the commit tx from the bitcoin network, extract your funding pubkey from its witness data
51+
* - recompute your channel keys and spend your output
52+
*/
53+
case object FundingPubKeyBasedChannelKeyPath extends ChannelConfigOption {
54+
override val supportBit: Int = 0
55+
override val name: String = "funding_pubkey_based_channel_keypath"
56+
}
57+
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2021 ACINQ SAS
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package fr.acinq.eclair.channel
18+
19+
import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey, Wumbo}
20+
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}
21+
import fr.acinq.eclair.{Feature, FeatureSupport, Features}
22+
23+
/**
24+
* Created by t-bast on 24/06/2021.
25+
*/
26+
27+
/**
28+
* Subset of Bolt 9 features used to configure a channel and applicable over the lifetime of that channel.
29+
* Even if one of these features is later disabled at the connection level, it will still apply to the channel until the
30+
* channel is upgraded or closed.
31+
*/
32+
case class ChannelFeatures(features: Features) {
33+
34+
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
35+
val paysDirectlyToWallet: Boolean = {
36+
features.hasFeature(Features.StaticRemoteKey) && !features.hasFeature(Features.AnchorOutputs)
37+
}
38+
39+
/** Format of the channel transactions. */
40+
val commitmentFormat: CommitmentFormat = {
41+
if (features.hasFeature(AnchorOutputs)) {
42+
AnchorOutputsCommitmentFormat
43+
} else {
44+
DefaultCommitmentFormat
45+
}
46+
}
47+
48+
def hasFeature(feature: Feature): Boolean = features.hasFeature(feature)
49+
50+
}
51+
52+
object ChannelFeatures {
53+
54+
/** Pick the channel features that should be used based on local and remote feature bits. */
55+
def pickChannelFeatures(localFeatures: Features, remoteFeatures: Features): ChannelFeatures = {
56+
// NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation,
57+
// such as option_dataloss_protect or option_shutdown_anysegwit.
58+
val availableFeatures: Seq[Feature] = Seq(
59+
StaticRemoteKey,
60+
Wumbo,
61+
AnchorOutputs,
62+
).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
63+
ChannelFeatures(Features(availableFeatures.map(f => f -> FeatureSupport.Mandatory).toMap))
64+
}
65+
66+
}

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

+17-59
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import fr.acinq.eclair.transactions.CommitmentSpec
2626
import fr.acinq.eclair.transactions.Transactions._
2727
import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
2828
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64}
29-
import scodec.bits.{BitVector, ByteVector}
29+
import scodec.bits.ByteVector
3030

3131
import java.util.UUID
3232

@@ -87,8 +87,14 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32,
8787
remote: ActorRef,
8888
remoteInit: Init,
8989
channelFlags: Byte,
90-
channelVersion: ChannelVersion)
91-
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelVersion: ChannelVersion)
90+
channelConfig: ChannelConfigOptions,
91+
channelFeatures: ChannelFeatures)
92+
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32,
93+
localParams: LocalParams,
94+
remote: ActorRef,
95+
remoteInit: Init,
96+
channelConfig: ChannelConfigOptions,
97+
channelFeatures: ChannelFeatures)
9298
case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close
9399
case object INPUT_DISCONNECTED
94100
case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init)
@@ -375,7 +381,8 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32
375381
initialFeeratePerKw: FeeratePerKw,
376382
initialRelayFees_opt: Option[(MilliSatoshi, Int)],
377383
remoteFirstPerCommitmentPoint: PublicKey,
378-
channelVersion: ChannelVersion,
384+
channelConfig: ChannelConfigOptions,
385+
channelFeatures: ChannelFeatures,
379386
lastSent: OpenChannel) extends Data {
380387
val channelId: ByteVector32 = temporaryChannelId
381388
}
@@ -388,7 +395,8 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32,
388395
initialRelayFees_opt: Option[(MilliSatoshi, Int)],
389396
remoteFirstPerCommitmentPoint: PublicKey,
390397
channelFlags: Byte,
391-
channelVersion: ChannelVersion,
398+
channelConfig: ChannelConfigOptions,
399+
channelFeatures: ChannelFeatures,
392400
lastSent: AcceptChannel) extends Data {
393401
val channelId: ByteVector32 = temporaryChannelId
394402
}
@@ -402,7 +410,8 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32,
402410
localCommitTx: CommitTx,
403411
remoteCommit: RemoteCommit,
404412
channelFlags: Byte,
405-
channelVersion: ChannelVersion,
413+
channelConfig: ChannelConfigOptions,
414+
channelFeatures: ChannelFeatures,
406415
lastSent: FundingCreated) extends Data
407416
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments,
408417
fundingTx: Option[Transaction],
@@ -445,8 +454,8 @@ final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Com
445454

446455
/**
447456
* @param features current connection features, or last features used if the channel is disconnected. Note that these
448-
* features are updated at each reconnection and may be different from the ones that were used when the
449-
* channel was created. See [[ChannelVersion]] for permanent features associated to a channel.
457+
* features are updated at each reconnection and may be different from the channel permanent features
458+
* (see [[ChannelFeatures]]).
450459
*/
451460
final case class LocalParams(nodeId: PublicKey,
452461
fundingKeyPath: DeterministicWallet.KeyPath,
@@ -482,55 +491,4 @@ object ChannelFlags {
482491
val AnnounceChannel = 0x01.toByte
483492
val Empty = 0x00.toByte
484493
}
485-
486-
case class ChannelVersion(bits: BitVector) {
487-
import ChannelVersion._
488-
489-
require(bits.size == ChannelVersion.LENGTH_BITS, "channel version takes 4 bytes")
490-
491-
val commitmentFormat: CommitmentFormat = if (hasAnchorOutputs) {
492-
AnchorOutputsCommitmentFormat
493-
} else {
494-
DefaultCommitmentFormat
495-
}
496-
497-
def |(other: ChannelVersion) = ChannelVersion(bits | other.bits)
498-
def &(other: ChannelVersion) = ChannelVersion(bits & other.bits)
499-
def ^(other: ChannelVersion) = ChannelVersion(bits ^ other.bits)
500-
501-
def isSet(bit: Int): Boolean = bits.reverse.get(bit)
502-
503-
def hasPubkeyKeyPath: Boolean = isSet(USE_PUBKEY_KEYPATH_BIT)
504-
def hasStaticRemotekey: Boolean = isSet(USE_STATIC_REMOTEKEY_BIT)
505-
def hasAnchorOutputs: Boolean = isSet(USE_ANCHOR_OUTPUTS_BIT)
506-
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
507-
def paysDirectlyToWallet: Boolean = hasStaticRemotekey && !hasAnchorOutputs
508-
}
509-
510-
object ChannelVersion {
511-
import scodec.bits._
512-
513-
val LENGTH_BITS: Int = 4 * 8
514-
515-
private val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0
516-
private val USE_STATIC_REMOTEKEY_BIT = 1
517-
private val USE_ANCHOR_OUTPUTS_BIT = 2
518-
519-
def fromBit(bit: Int): ChannelVersion = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse)
520-
521-
def pickChannelVersion(localFeatures: Features, remoteFeatures: Features): ChannelVersion = {
522-
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
523-
ANCHOR_OUTPUTS
524-
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
525-
STATIC_REMOTEKEY
526-
} else {
527-
STANDARD
528-
}
529-
}
530-
531-
val ZEROES = ChannelVersion(bin"00000000000000000000000000000000")
532-
val STANDARD = ZEROES | fromBit(USE_PUBKEY_KEYPATH_BIT)
533-
val STATIC_REMOTEKEY = STANDARD | fromBit(USE_STATIC_REMOTEKEY_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY
534-
val ANCHOR_OUTPUTS = STATIC_REMOTEKEY | fromBit(USE_ANCHOR_OUTPUTS_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + ANCHOR_OUTPUTS
535-
}
536494
// @formatter:on

0 commit comments

Comments
 (0)