Skip to content

Commit a297765

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 a297765

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

+712
-454
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.ChannelType
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 channelType channel type
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(channelType: ChannelType, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
42+
if (channelType.features.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 channelType channel type
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, channelType: ChannelType, 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 (channelType.features.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,77 @@
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 scodec.bits.{BitVector, ByteVector}
20+
21+
/**
22+
* Created by t-bast on 24/06/2021.
23+
*/
24+
25+
/**
26+
* Internal configuration option impacting the channel's structure or behavior.
27+
* This must be set when creating the channel and cannot be changed afterwards.
28+
*/
29+
trait ChannelConfigOption {
30+
def supportBit: Int
31+
}
32+
33+
case class ChannelConfigOptions(activated: Set[ChannelConfigOption]) {
34+
35+
def hasOption(option: ChannelConfigOption): Boolean = activated.contains(option)
36+
37+
def bytes: ByteVector = toByteVector
38+
39+
def toByteVector: ByteVector = {
40+
val indices = activated.map(_.supportBit)
41+
if (indices.isEmpty) {
42+
ByteVector.empty
43+
} else {
44+
// NB: when converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting bits.
45+
var buffer = BitVector.fill(indices.max + 1)(high = false).bytes.bits
46+
indices.foreach(i => buffer = buffer.set(i))
47+
buffer.reverse.bytes
48+
}
49+
}
50+
51+
}
52+
53+
object ChannelConfigOptions {
54+
55+
def standard: ChannelConfigOptions = ChannelConfigOptions(activated = Set(FundingPubKeyBasedChannelKeyPath))
56+
57+
def apply(options: ChannelConfigOption*): ChannelConfigOptions = ChannelConfigOptions(Set.from(options))
58+
59+
def apply(bytes: ByteVector): ChannelConfigOptions = {
60+
val activated: Set[ChannelConfigOption] = bytes.bits.toIndexedSeq.reverse.zipWithIndex.collect {
61+
case (true, 0) => FundingPubKeyBasedChannelKeyPath
62+
}.toSet
63+
ChannelConfigOptions(activated)
64+
}
65+
66+
/**
67+
* If set, the channel's BIP32 key path will be deterministically derived from the funding public key.
68+
* It makes it very easy to retrieve funds when channel data has been lost:
69+
* - connect to your peer and use option_data_loss_protect to get them to publish their remote commit tx
70+
* - retrieve the commit tx from the bitcoin network, extract your funding pubkey from its witness data
71+
* - recompute your channel keys and spend your output
72+
*/
73+
case object FundingPubKeyBasedChannelKeyPath extends ChannelConfigOption {
74+
override def supportBit = 0
75+
}
76+
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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.FeatureSupport.Optional
20+
import fr.acinq.eclair.Features
21+
import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey}
22+
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}
23+
24+
/**
25+
* Created by t-bast on 24/06/2021.
26+
*/
27+
28+
/**
29+
* A channel type is a specific combination of Bolt 9 features, defined in the RFC (Bolt 2).
30+
*/
31+
case class ChannelType(features: Features) {
32+
33+
val commitmentFormat: CommitmentFormat = {
34+
if (features.hasFeature(AnchorOutputs)) {
35+
AnchorOutputsCommitmentFormat
36+
} else {
37+
DefaultCommitmentFormat
38+
}
39+
}
40+
41+
/**
42+
* True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses.
43+
*/
44+
def paysDirectlyToWallet: Boolean = {
45+
features.hasFeature(Features.StaticRemoteKey) && !features.hasFeature(Features.AnchorOutputs)
46+
}
47+
48+
}
49+
50+
object ChannelTypes {
51+
52+
val standard = ChannelType(Features.empty)
53+
val staticRemoteKey = ChannelType(Features(StaticRemoteKey -> Optional))
54+
val anchorOutputs = ChannelType(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional))
55+
56+
/**
57+
* Pick the channel type that should be applied based on features alone (in case our peer doesn't support explicit channel type negotiation).
58+
*/
59+
def pickChannelType(localFeatures: Features, remoteFeatures: Features): ChannelType = {
60+
if (Features.canUseFeature(localFeatures, remoteFeatures, AnchorOutputs)) {
61+
anchorOutputs
62+
} else if (Features.canUseFeature(localFeatures, remoteFeatures, StaticRemoteKey)) {
63+
staticRemoteKey
64+
} else {
65+
standard
66+
}
67+
}
68+
69+
}

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+
channelType: ChannelType)
92+
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32,
93+
localParams: LocalParams,
94+
remote: ActorRef,
95+
remoteInit: Init,
96+
channelConfig: ChannelConfigOptions,
97+
channelType: ChannelType)
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+
channelType: ChannelType,
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+
channelType: ChannelType,
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+
channelType: ChannelType,
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+
* used to select the [[ChannelType]].
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)