Skip to content

Commit

Permalink
Explicit channel type in channel open
Browse files Browse the repository at this point in the history
Add support for lightning/bolts#880

This lets node operators open a channel with different features than what
the implicit choice based on activated features would use.
  • Loading branch information
t-bast committed Aug 13, 2021
1 parent 7f8062c commit 42f1bce
Show file tree
Hide file tree
Showing 19 changed files with 411 additions and 70 deletions.
5 changes: 3 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ trait Eclair {

def disconnect(nodeId: PublicKey)(implicit timeout: Timeout): Future[String]

def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse]
def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[ChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse]

def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]]

Expand Down Expand Up @@ -177,13 +177,14 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
(appKit.switchboard ? Peer.Disconnect(nodeId)).mapTo[String]
}

override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = {
override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[ChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = {
// we want the open timeout to expire *before* the default ask timeout, otherwise user won't get a generic response
val openTimeout = openTimeout_opt.getOrElse(Timeout(10 seconds))
(appKit.switchboard ? Peer.OpenChannel(
remoteNodeId = nodeId,
fundingSatoshis = fundingAmount,
pushMsat = pushAmount_opt.getOrElse(0 msat),
channelType_opt = channelType_opt,
fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)),
channelFlags = flags_opt.map(_.toByte),
timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse]
Expand Down
58 changes: 36 additions & 22 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
channelFlags = channelFlags,
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript)))
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript), ChannelTlv.ChannelType(channelFeatures.channelType.features)))
goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open

case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isFunder =>
Expand Down Expand Up @@ -363,7 +363,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey,
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript)))
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript), ChannelTlv.ChannelType(channelFeatures.channelType.features)))
val remoteParams = RemoteParams(
nodeId = remoteNodeId,
dustLimit = open.dustLimitSatoshis,
Expand Down Expand Up @@ -396,26 +396,40 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
Helpers.validateParamsFunder(nodeParams, channelFeatures, open, accept) match {
case Left(t) => handleLocalError(t, d, Some(accept))
case Right(remoteShutdownScript) =>
val remoteParams = RemoteParams(
nodeId = remoteNodeId,
dustLimit = accept.dustLimitSatoshis,
maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat,
channelReserve = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment
htlcMinimum = accept.htlcMinimumMsat,
toSelfDelay = accept.toSelfDelay,
maxAcceptedHtlcs = accept.maxAcceptedHtlcs,
fundingPubKey = accept.fundingPubkey,
revocationBasepoint = accept.revocationBasepoint,
paymentBasepoint = accept.paymentBasepoint,
delayedPaymentBasepoint = accept.delayedPaymentBasepoint,
htlcBasepoint = accept.htlcBasepoint,
initFeatures = remoteInit.features,
shutdownScript = remoteShutdownScript)
log.debug("remote params: {}", remoteParams)
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey)))
wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self)
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open)
// If we have overridden the default channel type, but they didn't support explicit channel type negotiation,
// we need to abort because they expect a different channel type than what we offered.
val channelTypeOk = (open.channelType_opt, accept.channelType_opt) match {
case (Some(proposedChannelType), None) =>
val channelTypeTheyExpect = ChannelTypes.pickChannelType(localParams.initFeatures, remoteInit.features)
channelTypeTheyExpect.features == proposedChannelType
case _ => true
}
if (!channelTypeOk) {
log.warning("open channel cancelled, peer doesn't support explicit channel type negotiation")
channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, peer doesn't support explicit channel type negotiation"))))
goto(CLOSED) sending Error(accept.temporaryChannelId, "explicit channel type negotiation not supported")
} else {
val remoteParams = RemoteParams(
nodeId = remoteNodeId,
dustLimit = accept.dustLimitSatoshis,
maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat,
channelReserve = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment
htlcMinimum = accept.htlcMinimumMsat,
toSelfDelay = accept.toSelfDelay,
maxAcceptedHtlcs = accept.maxAcceptedHtlcs,
fundingPubKey = accept.fundingPubkey,
revocationBasepoint = accept.revocationBasepoint,
paymentBasepoint = accept.paymentBasepoint,
delayedPaymentBasepoint = accept.delayedPaymentBasepoint,
htlcBasepoint = accept.htlcBasepoint,
initFeatures = remoteInit.features,
shutdownScript = remoteShutdownScript)
log.debug("remote params: {}", remoteParams)
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey)))
wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self)
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open)
}
}

case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32,
remoteInit: Init,
channelFlags: Byte,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures)
channelFeatures: ChannelFeatures) // TODO: maybe should be just the channel type?
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32,
localParams: LocalParams,
remote: ActorRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, Error, UpdateAddHtlc}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, UInt64}

/**
* Created by PM on 11/04/2017.
Expand All @@ -40,6 +40,7 @@ case class InvalidChainHash (override val channelId: Byte
case class InvalidFundingAmount (override val channelId: ByteVector32, fundingAmount: Satoshi, min: Satoshi, max: Satoshi) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingAmount (min=$min max=$max)")
case class InvalidPushAmount (override val channelId: ByteVector32, pushAmount: MilliSatoshi, max: MilliSatoshi) extends ChannelException(channelId, s"invalid pushAmount=$pushAmount (max=$max)")
case class InvalidMaxAcceptedHtlcs (override val channelId: ByteVector32, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)")
case class InvalidChannelType (override val channelId: ByteVector32, channelType: Features) extends ChannelException(channelId, s"invalid channel_type=0x${channelType.toByteVector.toHex}")
case class DustLimitTooSmall (override val channelId: ByteVector32, dustLimit: Satoshi, min: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too small (min=$min)")
case class DustLimitTooLarge (override val channelId: ByteVector32, dustLimit: Satoshi, max: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too large (max=$max)")
case class DustLimitAboveOurChannelReserve (override val channelId: ByteVector32, dustLimit: Satoshi, channelReserve: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is above our channelReserve=$channelReserve")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel

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

/**
* Created by t-bast on 24/06/2021.
Expand All @@ -45,6 +45,16 @@ case class ChannelFeatures(activated: Set[Feature]) {
}
}

val channelType: ChannelType = {
if (hasFeature(AnchorOutputs)) {
ChannelTypes.AnchorOutputs
} else if (hasFeature(StaticRemoteKey)) {
ChannelTypes.StaticRemoteKey
} else {
ChannelTypes.Standard
}
}

def hasFeature(feature: Feature): Boolean = activated.contains(feature)

override def toString: String = activated.mkString(",")
Expand All @@ -55,6 +65,15 @@ object ChannelFeatures {

def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features))

/** Enrich the channel type with other permanent features that will be applied to the channel. */
def apply(channelType: ChannelType, 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(Wumbo, OptionUpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
val allFeatures = channelType.features.activated.keys.toSeq ++ availableFeatures
ChannelFeatures(allFeatures: _*)
}

/** 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,
Expand All @@ -70,3 +89,46 @@ object ChannelFeatures {
}

}

/** A channel type is a specific set of even feature bits that represent persistent channel features as defined in Bolt 2. */
sealed trait ChannelType {
def features: Features
}

object ChannelTypes {

// @formatter:off
case object Standard extends ChannelType {
override def features: Features = Features.empty
override def toString: String = "standard"
}
case object StaticRemoteKey extends ChannelType {
override def features: Features = Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory)
override def toString: String = "static_remotekey"
}
case object AnchorOutputs extends ChannelType {
override def features: Features = Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory)
override def toString: String = "anchor_outputs"
}
// @formatter:on

// NB: Bolt 2: features must exactly match in order to identify a channel type.
def fromFeatures(features: Features): Option[ChannelType] = features match {
case f if f == AnchorOutputs.features => Some(AnchorOutputs)
case f if f == StaticRemoteKey.features => Some(StaticRemoteKey)
case f if f == Standard.features => Some(Standard)
case _ => None
}

/** Pick the channel type based on local and remote feature bits. */
def pickChannelType(localFeatures: Features, remoteFeatures: Features): ChannelType = {
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
AnchorOutputs
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
StaticRemoteKey
} else {
Standard
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ object Helpers {
val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1)
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio))

// if channel_type is set, and channel_type was set in open_channel, and they are not equal types: MUST reject the channel.
accept.channelType_opt match {
case Some(theirChannelType) if accept.channelType_opt != open.channelType_opt => return Left(InvalidChannelType(open.temporaryChannelId, theirChannelType))
case _ => // nothing to do
}

extractShutdownScript(accept.temporaryChannelId, channelFeatures, accept.upfrontShutdownScript_opt)
}

Expand Down
Loading

0 comments on commit 42f1bce

Please sign in to comment.