Skip to content

Commit 59ccf34

Browse files
authored
Explicit channel type in channel open (#1867)
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 275581d commit 59ccf34

31 files changed

+576
-197
lines changed

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ trait Eclair {
9090

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

93-
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]
93+
def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse]
9494

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

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

180-
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] = {
180+
override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = {
181181
// we want the open timeout to expire *before* the default ask timeout, otherwise user won't get a generic response
182182
val openTimeout = openTimeout_opt.getOrElse(Timeout(10 seconds))
183183
(appKit.switchboard ? Peer.OpenChannel(
184184
remoteNodeId = nodeId,
185185
fundingSatoshis = fundingAmount,
186186
pushMsat = pushAmount_opt.getOrElse(0 msat),
187+
channelType_opt = channelType_opt,
187188
fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)),
188189
channelFlags = flags_opt.map(_.toByte),
189190
timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse]

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala

+13-11
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ 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
2221
import fr.acinq.eclair.blockchain.CurrentFeerates
23-
import fr.acinq.eclair.channel.ChannelFeatures
22+
import fr.acinq.eclair.channel.{ChannelType, ChannelTypes, SupportedChannelType}
2423

2524
trait FeeEstimator {
2625
// @formatter:off
@@ -33,16 +32,19 @@ case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutua
3332

3433
case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMaxCommitFeerate: FeeratePerKw) {
3534
/**
36-
* @param channelFeatures permanent channel features
35+
* @param channelType channel type
3736
* @param networkFeerate reference fee rate (value we estimate from our view of the network)
3837
* @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx)
3938
* @return true if the difference between proposed and reference fee rates is too high.
4039
*/
41-
def isFeeDiffTooHigh(channelFeatures: ChannelFeatures, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
42-
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
43-
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
44-
} else {
45-
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
40+
def isFeeDiffTooHigh(channelType: SupportedChannelType, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
41+
channelType match {
42+
case ChannelTypes.Standard =>
43+
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
44+
case ChannelTypes.StaticRemoteKey =>
45+
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
46+
case ChannelTypes.AnchorOutputs =>
47+
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
4648
}
4749
}
4850
}
@@ -61,15 +63,15 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl
6163
* - otherwise we use a feerate that should get the commit tx confirmed within the configured block target
6264
*
6365
* @param remoteNodeId nodeId of our channel peer
64-
* @param channelFeatures permanent channel features
66+
* @param channelType channel type
6567
* @param currentFeerates_opt if provided, will be used to compute the most up-to-date network fee, otherwise we rely on the fee estimator
6668
*/
67-
def getCommitmentFeerate(remoteNodeId: PublicKey, channelFeatures: ChannelFeatures, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
69+
def getCommitmentFeerate(remoteNodeId: PublicKey, channelType: ChannelType, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
6870
val networkFeerate = currentFeerates_opt match {
6971
case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget)
7072
case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget)
7173
}
72-
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
74+
if (channelType == ChannelTypes.AnchorOutputs) {
7375
networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
7476
} else {
7577
networkFeerate

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

+23-19
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,15 @@ 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, channelFeatures), Nothing) =>
198+
case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) =>
199199
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw)))
200200
activeConnection = remote
201201
txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId)
202202
val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
203203
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
204204
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used
205205
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
206-
val localShutdownScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty
206+
val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty
207207
val open = OpenChannel(nodeParams.chainHash,
208208
temporaryChannelId = temporaryChannelId,
209209
fundingSatoshis = fundingSatoshis,
@@ -222,7 +222,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
222222
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
223223
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
224224
channelFlags = channelFlags,
225-
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript)))
225+
tlvStream = TlvStream(
226+
ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript),
227+
ChannelTlv.ChannelTypeTlv(channelType)
228+
))
226229
goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open
227230

228231
case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isFunder =>
@@ -337,18 +340,17 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
337340
})
338341

339342
when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions {
340-
case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelFeatures))) =>
341-
log.info("received OpenChannel={}", open)
342-
Helpers.validateParamsFundee(nodeParams, localParams.initFeatures, channelFeatures, open, remoteNodeId) match {
343+
case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelType))) =>
344+
Helpers.validateParamsFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match {
343345
case Left(t) => handleLocalError(t, d, Some(open))
344-
case Right(remoteShutdownScript) =>
346+
case Right((channelFeatures, remoteShutdownScript)) =>
345347
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None))
346348
val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
347349
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
348350
val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis)
349351
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used.
350352
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
351-
val localShutdownScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty
353+
val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty
352354
val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId,
353355
dustLimitSatoshis = localParams.dustLimit,
354356
maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat,
@@ -363,7 +365,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
363365
delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey,
364366
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
365367
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
366-
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript)))
368+
tlvStream = TlvStream(
369+
ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript),
370+
ChannelTlv.ChannelTypeTlv(channelType)
371+
))
367372
val remoteParams = RemoteParams(
368373
nodeId = remoteNodeId,
369374
dustLimit = open.dustLimitSatoshis,
@@ -391,11 +396,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
391396
})
392397

393398
when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions {
394-
case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelFeatures), open)) =>
395-
log.info(s"received AcceptChannel=$accept")
396-
Helpers.validateParamsFunder(nodeParams, channelFeatures, open, accept) match {
399+
case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelType), open)) =>
400+
Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match {
397401
case Left(t) => handleLocalError(t, d, Some(accept))
398-
case Right(remoteShutdownScript) =>
402+
case Right((channelFeatures, remoteShutdownScript)) =>
399403
val remoteParams = RemoteParams(
400404
nodeId = remoteNodeId,
401405
dustLimit = accept.dustLimitSatoshis,
@@ -889,7 +893,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
889893
case Event(remoteShutdown@Shutdown(_, remoteScriptPubKey, _), d: DATA_NORMAL) =>
890894
d.commitments.getRemoteShutdownScript(remoteScriptPubKey) match {
891895
case Left(e) =>
892-
log.warning("they sent an invalid closing script")
896+
log.warning(s"they sent an invalid closing script: ${e.getMessage}")
893897
context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId))
894898
stay() sending Warning(d.channelId, "invalid closing script")
895899
case Right(remoteShutdownScript) =>
@@ -1681,7 +1685,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
16811685
val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty
16821686
if (d.commitments.localParams.isFunder && !shutdownInProgress) {
16831687
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
1684-
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, None)
1688+
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, None)
16851689
if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) {
16861690
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
16871691
}
@@ -1972,11 +1976,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
19721976
}
19731977

19741978
private def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = {
1975-
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, Some(c))
1979+
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c))
19761980
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
19771981
val shouldUpdateFee = d.commitments.localParams.isFunder && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)
19781982
val shouldClose = !d.commitments.localParams.isFunder &&
1979-
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelFeatures, networkFeeratePerKw, currentFeeratePerKw) &&
1983+
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) &&
19801984
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
19811985
if (shouldUpdateFee) {
19821986
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
@@ -1996,11 +2000,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
19962000
* @return
19972001
*/
19982002
private def handleOfflineFeerate(c: CurrentFeerates, d: HasCommitments) = {
1999-
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, Some(c))
2003+
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c))
20002004
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
20012005
// if the network fees are too high we risk to not be able to confirm our current commitment
20022006
val shouldClose = networkFeeratePerKw > currentFeeratePerKw &&
2003-
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelFeatures, networkFeeratePerKw, currentFeeratePerKw) &&
2007+
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) &&
20042008
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
20052009
if (shouldClose) {
20062010
if (nodeParams.onChainFeeConf.closeOnOfflineMismatch) {

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,13 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32,
8787
remoteInit: Init,
8888
channelFlags: Byte,
8989
channelConfig: ChannelConfig,
90-
channelFeatures: ChannelFeatures)
90+
channelType: SupportedChannelType)
9191
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32,
9292
localParams: LocalParams,
9393
remote: ActorRef,
9494
remoteInit: Init,
9595
channelConfig: ChannelConfig,
96-
channelFeatures: ChannelFeatures)
96+
channelType: SupportedChannelType)
9797
case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close
9898
case object INPUT_DISCONNECTED
9999
case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init)

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

+1
Original file line numberDiff line numberDiff line change
@@ -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, ourChannelType: ChannelType, theirChannelType: ChannelType) extends ChannelException(channelId, s"invalid channel_type=$theirChannelType, expected channel_type=$ourChannelType")
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")

0 commit comments

Comments
 (0)