Skip to content

Commit 37d28e8

Browse files
committed
Explicit channel type in channel open
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.
1 parent 4e0c814 commit 37d28e8

File tree

24 files changed

+490
-169
lines changed

24 files changed

+490
-169
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ trait Eclair {
8585

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

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

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

@@ -170,13 +170,14 @@ class EclairImpl(appKit: Kit) extends Eclair {
170170
(appKit.switchboard ? Peer.Disconnect(nodeId)).mapTo[String]
171171
}
172172

173-
override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], initialRelayFees_opt: Option[(MilliSatoshi, Int)], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = {
173+
override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[ChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], initialRelayFees_opt: Option[(MilliSatoshi, Int)], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = {
174174
// we want the open timeout to expire *before* the default ask timeout, otherwise user won't get a generic response
175175
val openTimeout = openTimeout_opt.getOrElse(Timeout(10 seconds))
176176
(appKit.switchboard ? Peer.OpenChannel(
177177
remoteNodeId = nodeId,
178178
fundingSatoshis = fundingAmount,
179179
pushMsat = pushAmount_opt.getOrElse(0 msat),
180+
channelType_opt = channelType_opt,
180181
fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)),
181182
initialRelayFees_opt = initialRelayFees_opt,
182183
channelFlags = flags_opt.map(_.toByte),

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

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
195195
startWith(WAIT_FOR_INIT_INTERNAL, Nothing)
196196

197197
when(WAIT_FOR_INIT_INTERNAL)(handleExceptions {
198-
case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, _, localParams, remote, _, channelFlags, channelConfig, _), Nothing) =>
198+
case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, _, localParams, remote, _, channelFlags, channelConfig, channelFeatures), Nothing) =>
199199
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw)))
200200
activeConnection = remote
201201
txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId)
@@ -221,7 +221,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
221221
channelFlags = channelFlags,
222222
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script.
223223
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
224-
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))
224+
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty), ChannelTlv.ChannelType(channelFeatures.channelType.features)))
225225
goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open
226226

227227
case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isFunder =>
@@ -362,7 +362,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
362362
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
363363
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script.
364364
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
365-
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))
365+
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty), ChannelTlv.ChannelType(channelFeatures.channelType.features)))
366366
val remoteParams = RemoteParams(
367367
nodeId = remoteNodeId,
368368
dustLimit = open.dustLimitSatoshis,
@@ -395,26 +395,40 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
395395
Helpers.validateParamsFunder(nodeParams, open, accept) match {
396396
case Left(t) => handleLocalError(t, d, Some(accept))
397397
case _ =>
398-
val remoteParams = RemoteParams(
399-
nodeId = remoteNodeId,
400-
dustLimit = accept.dustLimitSatoshis,
401-
maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat,
402-
channelReserve = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment
403-
htlcMinimum = accept.htlcMinimumMsat,
404-
toSelfDelay = accept.toSelfDelay,
405-
maxAcceptedHtlcs = accept.maxAcceptedHtlcs,
406-
fundingPubKey = accept.fundingPubkey,
407-
revocationBasepoint = accept.revocationBasepoint,
408-
paymentBasepoint = accept.paymentBasepoint,
409-
delayedPaymentBasepoint = accept.delayedPaymentBasepoint,
410-
htlcBasepoint = accept.htlcBasepoint,
411-
features = remoteInit.features,
412-
shutdownScript = None)
413-
log.debug("remote params: {}", remoteParams)
414-
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
415-
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey)))
416-
wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self)
417-
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, initialRelayFees_opt, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open)
398+
// If we have overridden the default channel type, but they didn't support explicit channel type negotiation,
399+
// we need to abort because they expect a different channel type than what we offered.
400+
val channelTypeOk = (open.channelType_opt, accept.channelType_opt) match {
401+
case (Some(proposedChannelType), None) =>
402+
val channelTypeTheyExpect = ChannelTypes.pickChannelType(localParams.features, remoteInit.features)
403+
channelTypeTheyExpect.features == proposedChannelType
404+
case _ => true
405+
}
406+
if (!channelTypeOk) {
407+
log.warning("open channel cancelled, peer doesn't support explicit channel type negotiation")
408+
channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, peer doesn't support explicit channel type negotiation"))))
409+
goto(CLOSED) sending Error(accept.temporaryChannelId, "explicit channel type negotiation not supported")
410+
} else {
411+
val remoteParams = RemoteParams(
412+
nodeId = remoteNodeId,
413+
dustLimit = accept.dustLimitSatoshis,
414+
maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat,
415+
channelReserve = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment
416+
htlcMinimum = accept.htlcMinimumMsat,
417+
toSelfDelay = accept.toSelfDelay,
418+
maxAcceptedHtlcs = accept.maxAcceptedHtlcs,
419+
fundingPubKey = accept.fundingPubkey,
420+
revocationBasepoint = accept.revocationBasepoint,
421+
paymentBasepoint = accept.paymentBasepoint,
422+
delayedPaymentBasepoint = accept.delayedPaymentBasepoint,
423+
htlcBasepoint = accept.htlcBasepoint,
424+
features = remoteInit.features,
425+
shutdownScript = None)
426+
log.debug("remote params: {}", remoteParams)
427+
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
428+
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey)))
429+
wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self)
430+
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, initialRelayFees_opt, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open)
431+
}
418432
}
419433

420434
case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) =>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import fr.acinq.bitcoin.Crypto.PrivateKey
2020
import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction}
2121
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
2222
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, Error, UpdateAddHtlc}
23-
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64}
23+
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, UInt64}
2424

2525
/**
2626
* Created by PM on 11/04/2017.
@@ -40,6 +40,7 @@ case class InvalidChainHash (override val channelId: Byte
4040
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)")
4141
case class InvalidPushAmount (override val channelId: ByteVector32, pushAmount: MilliSatoshi, max: MilliSatoshi) extends ChannelException(channelId, s"invalid pushAmount=$pushAmount (max=$max)")
4242
case class InvalidMaxAcceptedHtlcs (override val channelId: ByteVector32, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)")
43+
case class InvalidChannelType (override val channelId: ByteVector32, channelType: Features) extends ChannelException(channelId, s"invalid channel_type=0x${channelType.toByteVector.toHex}")
4344
case class DustLimitTooSmall (override val channelId: ByteVector32, dustLimit: Satoshi, min: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too small (min=$min)")
4445
case class DustLimitTooLarge (override val channelId: ByteVector32, dustLimit: Satoshi, max: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too large (max=$max)")
4546
case class DustLimitAboveOurChannelReserve (override val channelId: ByteVector32, dustLimit: Satoshi, channelReserve: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is above our channelReserve=$channelReserve")

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

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,77 @@ case class ChannelFeatures(features: Features) {
4545
}
4646
}
4747

48+
val channelType: ChannelType = {
49+
if (features.hasFeature(AnchorOutputs)) {
50+
ChannelTypes.AnchorOutputs
51+
} else if (features.hasFeature(StaticRemoteKey)) {
52+
ChannelTypes.StaticRemoteKey
53+
} else {
54+
ChannelTypes.Standard
55+
}
56+
}
57+
4858
def hasFeature(feature: Feature): Boolean = features.hasFeature(feature)
4959

5060
}
5161

5262
object ChannelFeatures {
5363

54-
/** Pick the channel features that should be used based on local and remote feature bits. */
55-
def pickChannelFeatures(localFeatures: Features, remoteFeatures: Features): ChannelFeatures = {
64+
/** Enrich the channel type with other permanent features that will be applied to the channel. */
65+
def apply(channelType: ChannelType, localFeatures: Features, remoteFeatures: Features): ChannelFeatures = {
5666
// NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation,
5767
// 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))
68+
val availableFeatures: Map[Feature, FeatureSupport] = Seq(Wumbo).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)).map(f => f -> FeatureSupport.Mandatory).toMap
69+
val allFeatures = channelType.features.copy(activated = channelType.features.activated ++ availableFeatures)
70+
ChannelFeatures(allFeatures)
71+
}
72+
73+
}
74+
75+
/** A channel type is a specific set of even feature bits that represent persistent channel features as defined in Bolt 2. */
76+
sealed trait ChannelType {
77+
def features: Features
78+
}
79+
80+
// TODO: rename (in separate PR):
81+
// - State -> ChannelState
82+
// - Data -> ChannelData
83+
// - ChannelTypes.scala -> ChannelData.scala
84+
85+
object ChannelTypes {
86+
87+
// @formatter:off
88+
case object Standard extends ChannelType {
89+
override def features: Features = Features.empty
90+
override def toString: String = "standard"
91+
}
92+
case object StaticRemoteKey extends ChannelType {
93+
override def features: Features = Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory)
94+
override def toString: String = "static_remotekey"
95+
}
96+
case object AnchorOutputs extends ChannelType {
97+
override def features: Features = Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory)
98+
override def toString: String = "anchor_outputs"
99+
}
100+
// @formatter:on
101+
102+
// NB: Bolt 2: features must exactly match in order to identify a channel type.
103+
def fromFeatures(features: Features): Option[ChannelType] = features match {
104+
case f if f == AnchorOutputs.features => Some(AnchorOutputs)
105+
case f if f == StaticRemoteKey.features => Some(StaticRemoteKey)
106+
case f if f == Standard.features => Some(Standard)
107+
case _ => None
108+
}
109+
110+
/** Pick the channel type based on local and remote feature bits. */
111+
def pickChannelType(localFeatures: Features, remoteFeatures: Features): ChannelType = {
112+
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
113+
AnchorOutputs
114+
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
115+
StaticRemoteKey
116+
} else {
117+
Standard
118+
}
64119
}
65120

66121
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ object Helpers {
162162
val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1)
163163
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio))
164164

165+
// if channel_type is set, and channel_type was set in open_channel, and they are not equal types: MUST reject the channel.
166+
accept.channelType_opt match {
167+
case Some(theirChannelType) if accept.channelType_opt != open.channelType_opt => return Left(InvalidChannelType(open.temporaryChannelId, theirChannelType))
168+
case _ => // nothing to do
169+
}
170+
165171
Right()
166172
}
167173

0 commit comments

Comments
 (0)