Skip to content

Commit

Permalink
Separate internal channel config from features (#1852)
Browse files Browse the repository at this point in the history
* 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).

* Add shutdown script to channel remote params

This will be necessary when we implement `upfront_shutdown_script`.
This field can be set to `None` for all current channels as we don't support
the feature yet.
  • Loading branch information
t-bast authored Jul 6, 2021
1 parent 2e074f7 commit e88e6c5
Show file tree
Hide file tree
Showing 44 changed files with 848 additions and 495 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ package fr.acinq.eclair.blockchain.fee

import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Satoshi
import fr.acinq.eclair.Features
import fr.acinq.eclair.blockchain.CurrentFeerates
import fr.acinq.eclair.channel.ChannelVersion
import fr.acinq.eclair.channel.ChannelFeatures

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

case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMaxCommitFeerate: FeeratePerKw) {
/**
* @param channelVersion channel version
* @param channelFeatures permanent channel features
* @param networkFeerate reference fee rate (value we estimate from our view of the network)
* @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx)
* @return true if the difference between proposed and reference fee rates is too high.
*/
def isFeeDiffTooHigh(channelVersion: ChannelVersion, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
if (channelVersion.hasAnchorOutputs) {
def isFeeDiffTooHigh(channelFeatures: ChannelFeatures, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
} else {
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
Expand All @@ -60,15 +61,15 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl
* - otherwise we use a feerate that should get the commit tx confirmed within the configured block target
*
* @param remoteNodeId nodeId of our channel peer
* @param channelVersion channel version
* @param channelFeatures permanent channel features
* @param currentFeerates_opt if provided, will be used to compute the most up-to-date network fee, otherwise we rely on the fee estimator
*/
def getCommitmentFeerate(remoteNodeId: PublicKey, channelVersion: ChannelVersion, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
def getCommitmentFeerate(remoteNodeId: PublicKey, channelFeatures: ChannelFeatures, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
val networkFeerate = currentFeerates_opt match {
case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget)
case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget)
}
if (channelVersion.hasAnchorOutputs) {
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
} else {
networkFeerate
Expand Down
72 changes: 37 additions & 35 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2021 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.channel

/**
* Created by t-bast on 24/06/2021.
*/

/**
* Internal configuration option impacting the channel's structure or behavior.
* This must be set when creating the channel and cannot be changed afterwards.
*/
trait ChannelConfigOption {
// @formatter:off
def supportBit: Int
def name: String
// @formatter:on
}

case class ChannelConfig(options: Set[ChannelConfigOption]) {

def hasOption(option: ChannelConfigOption): Boolean = options.contains(option)

}

object ChannelConfig {

val standard: ChannelConfig = ChannelConfig(options = Set(FundingPubKeyBasedChannelKeyPath))

def apply(opts: ChannelConfigOption*): ChannelConfig = ChannelConfig(Set.from(opts))

/**
* If set, the channel's BIP32 key path will be deterministically derived from the funding public key.
* It makes it very easy to retrieve funds when channel data has been lost:
* - connect to your peer and use option_data_loss_protect to get them to publish their remote commit tx
* - retrieve the commit tx from the bitcoin network, extract your funding pubkey from its witness data
* - recompute your channel keys and spend your output
*/
case object FundingPubKeyBasedChannelKeyPath extends ChannelConfigOption {
override val supportBit: Int = 0
override val name: String = "funding_pubkey_based_channel_keypath"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2021 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.channel

import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey, Wumbo}
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}
import fr.acinq.eclair.{Feature, FeatureSupport, Features}

/**
* Created by t-bast on 24/06/2021.
*/

/**
* Subset of Bolt 9 features used to configure a channel and applicable over the lifetime of that channel.
* Even if one of these features is later disabled at the connection level, it will still apply to the channel until the
* channel is upgraded or closed.
*/
case class ChannelFeatures(features: Features) {

/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
val paysDirectlyToWallet: Boolean = {
features.hasFeature(Features.StaticRemoteKey) && !features.hasFeature(Features.AnchorOutputs)
}

/** Format of the channel transactions. */
val commitmentFormat: CommitmentFormat = {
if (features.hasFeature(AnchorOutputs)) {
AnchorOutputsCommitmentFormat
} else {
DefaultCommitmentFormat
}
}

def hasFeature(feature: Feature): Boolean = features.hasFeature(feature)

}

object ChannelFeatures {

/** Pick the channel features that should be used based on local and remote feature bits. */
def pickChannelFeatures(localFeatures: Features, remoteFeatures: Features): ChannelFeatures = {
// NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation,
// such as option_dataloss_protect or option_shutdown_anysegwit.
val availableFeatures: Seq[Feature] = Seq(
StaticRemoteKey,
Wumbo,
AnchorOutputs,
).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
ChannelFeatures(Features(availableFeatures.map(f => f -> FeatureSupport.Mandatory).toMap))
}

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

import java.util.UUID

Expand Down Expand Up @@ -87,8 +87,14 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32,
remote: ActorRef,
remoteInit: Init,
channelFlags: Byte,
channelVersion: ChannelVersion)
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelVersion: ChannelVersion)
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures)
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32,
localParams: LocalParams,
remote: ActorRef,
remoteInit: Init,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures)
case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close
case object INPUT_DISCONNECTED
case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init)
Expand Down Expand Up @@ -375,7 +381,8 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32
initialFeeratePerKw: FeeratePerKw,
initialRelayFees_opt: Option[(MilliSatoshi, Int)],
remoteFirstPerCommitmentPoint: PublicKey,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
lastSent: OpenChannel) extends Data {
val channelId: ByteVector32 = temporaryChannelId
}
Expand All @@ -388,7 +395,8 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32,
initialRelayFees_opt: Option[(MilliSatoshi, Int)],
remoteFirstPerCommitmentPoint: PublicKey,
channelFlags: Byte,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
lastSent: AcceptChannel) extends Data {
val channelId: ByteVector32 = temporaryChannelId
}
Expand All @@ -402,7 +410,8 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32,
localCommitTx: CommitTx,
remoteCommit: RemoteCommit,
channelFlags: Byte,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
lastSent: FundingCreated) extends Data
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments,
fundingTx: Option[Transaction],
Expand Down Expand Up @@ -445,8 +454,8 @@ final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Com

/**
* @param features current connection features, or last features used if the channel is disconnected. Note that these
* features are updated at each reconnection and may be different from the ones that were used when the
* channel was created. See [[ChannelVersion]] for permanent features associated to a channel.
* features are updated at each reconnection and may be different from the channel permanent features
* (see [[ChannelFeatures]]).
*/
final case class LocalParams(nodeId: PublicKey,
fundingKeyPath: DeterministicWallet.KeyPath,
Expand Down Expand Up @@ -476,61 +485,11 @@ final case class RemoteParams(nodeId: PublicKey,
paymentBasepoint: PublicKey,
delayedPaymentBasepoint: PublicKey,
htlcBasepoint: PublicKey,
features: Features)
features: Features,
shutdownScript: Option[ByteVector])

object ChannelFlags {
val AnnounceChannel = 0x01.toByte
val Empty = 0x00.toByte
}

case class ChannelVersion(bits: BitVector) {
import ChannelVersion._

require(bits.size == ChannelVersion.LENGTH_BITS, "channel version takes 4 bytes")

val commitmentFormat: CommitmentFormat = if (hasAnchorOutputs) {
AnchorOutputsCommitmentFormat
} else {
DefaultCommitmentFormat
}

def |(other: ChannelVersion) = ChannelVersion(bits | other.bits)
def &(other: ChannelVersion) = ChannelVersion(bits & other.bits)
def ^(other: ChannelVersion) = ChannelVersion(bits ^ other.bits)

def isSet(bit: Int): Boolean = bits.reverse.get(bit)

def hasPubkeyKeyPath: Boolean = isSet(USE_PUBKEY_KEYPATH_BIT)
def hasStaticRemotekey: Boolean = isSet(USE_STATIC_REMOTEKEY_BIT)
def hasAnchorOutputs: Boolean = isSet(USE_ANCHOR_OUTPUTS_BIT)
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
def paysDirectlyToWallet: Boolean = hasStaticRemotekey && !hasAnchorOutputs
}

object ChannelVersion {
import scodec.bits._

val LENGTH_BITS: Int = 4 * 8

private val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0
private val USE_STATIC_REMOTEKEY_BIT = 1
private val USE_ANCHOR_OUTPUTS_BIT = 2

def fromBit(bit: Int): ChannelVersion = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse)

def pickChannelVersion(localFeatures: Features, remoteFeatures: Features): ChannelVersion = {
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
ANCHOR_OUTPUTS
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
STATIC_REMOTEKEY
} else {
STANDARD
}
}

val ZEROES = ChannelVersion(bin"00000000000000000000000000000000")
val STANDARD = ZEROES | fromBit(USE_PUBKEY_KEYPATH_BIT)
val STATIC_REMOTEKEY = STANDARD | fromBit(USE_STATIC_REMOTEKEY_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY
val ANCHOR_OUTPUTS = STATIC_REMOTEKEY | fromBit(USE_ANCHOR_OUTPUTS_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + ANCHOR_OUTPUTS
}
// @formatter:on
Loading

0 comments on commit e88e6c5

Please sign in to comment.