Skip to content

Commit

Permalink
Separate internal channel config from features
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
t-bast committed Jun 24, 2021
1 parent 45204e2 commit b6df708
Show file tree
Hide file tree
Showing 16 changed files with 461 additions and 220 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.ChannelType

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 channelType channel type
* @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(channelType: ChannelType, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
if (channelType.features.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 channelType channel type
* @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, channelType: ChannelType, 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 (channelType.features.hasFeature(Features.AnchorOutputs)) {
networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
} else {
networkFeerate
Expand Down
66 changes: 33 additions & 33 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,77 @@
/*
* 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 scodec.bits.{BitVector, ByteVector}

/**
* 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 {
def supportBit: Int
}

case class ChannelConfigOptions(activated: Set[ChannelConfigOption]) {

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

def bytes: ByteVector = toByteVector

def toByteVector: ByteVector = {
val indices = activated.map(_.supportBit)
if (indices.isEmpty) {
ByteVector.empty
} else {
// NB: when converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting bits.
var buffer = BitVector.fill(indices.max + 1)(high = false).bytes.bits
indices.foreach(i => buffer = buffer.set(i))
buffer.reverse.bytes
}
}

}

object ChannelConfigOptions {

def standard: ChannelConfigOptions = ChannelConfigOptions(activated = Set(FundingPubKeyBasedChannelKeyPath))

def apply(options: ChannelConfigOption*): ChannelConfigOptions = ChannelConfigOptions(Set.from(options))

def apply(bytes: ByteVector): ChannelConfigOptions = {
val activated: Set[ChannelConfigOption] = bytes.bits.toIndexedSeq.reverse.zipWithIndex.collect {
case (true, 0) => FundingPubKeyBasedChannelKeyPath
}.toSet
ChannelConfigOptions(activated)
}

/**
* 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 def supportBit = 0
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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.FeatureSupport.Optional
import fr.acinq.eclair.Features
import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey}
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}

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

/**
* A channel type is a specific combination of Bolt 9 features, defined in the RFC (Bolt 2).
*/
case class ChannelType(features: Features) {

val commitmentFormat: CommitmentFormat = {
if (features.hasFeature(AnchorOutputs)) {
AnchorOutputsCommitmentFormat
} else {
DefaultCommitmentFormat
}
}

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

}

object ChannelTypes {

val standard = ChannelType(Features.empty)
val staticRemoteKey = ChannelType(Features(StaticRemoteKey -> Optional))
val anchorOutputs = ChannelType(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional))

/**
* Pick the channel type that should be applied based on features alone (in case our peer doesn't support explicit channel type negotiation).
*/
def pickChannelType(localFeatures: Features, remoteFeatures: Features): ChannelType = {
if (Features.canUseFeature(localFeatures, remoteFeatures, AnchorOutputs)) {
anchorOutputs
} else if (Features.canUseFeature(localFeatures, remoteFeatures, StaticRemoteKey)) {
staticRemoteKey
} else {
standard
}
}

}
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: ChannelConfigOptions,
supportedChannelTypes: Seq[ChannelType])
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32,
localParams: LocalParams,
remote: ActorRef,
remoteInit: Init,
channelConfig: ChannelConfigOptions,
channelType: ChannelType)
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: ChannelConfigOptions,
channelType: ChannelType,
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: ChannelConfigOptions,
channelType: ChannelType,
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: ChannelConfigOptions,
channelType: ChannelType,
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
* used to select the [[ChannelType]].
*/
final case class LocalParams(nodeId: PublicKey,
fundingKeyPath: DeterministicWallet.KeyPath,
Expand Down Expand Up @@ -482,55 +491,4 @@ 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 b6df708

Please sign in to comment.