Skip to content

Commit 28618e7

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 aab83fd commit 28618e7

31 files changed

+507
-158
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
@@ -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[ChannelType], 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[ChannelType], 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

Lines changed: 13 additions & 11 deletions
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}
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: ChannelType, 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

Lines changed: 16 additions & 18 deletions
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,7 @@ 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(ChannelTlv.UpfrontShutdownScript(localShutdownScript), ChannelTlv.ChannelType(channelType.features)))
226226
goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open
227227

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

339339
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 {
340+
case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelType))) =>
341+
Helpers.validateParamsFundee(nodeParams, localParams.initFeatures, channelType, open, remoteNodeId, remoteInit.features) match {
343342
case Left(t) => handleLocalError(t, d, Some(open))
344-
case Right(remoteShutdownScript) =>
343+
case Right((channelFeatures, remoteShutdownScript)) =>
345344
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None))
346345
val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
347346
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
348347
val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis)
349348
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used.
350349
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
351-
val localShutdownScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty
350+
val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty
352351
val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId,
353352
dustLimitSatoshis = localParams.dustLimit,
354353
maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat,
@@ -363,7 +362,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
363362
delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey,
364363
htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey,
365364
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
366-
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript)))
365+
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript), ChannelTlv.ChannelType(channelType.features)))
367366
val remoteParams = RemoteParams(
368367
nodeId = remoteNodeId,
369368
dustLimit = open.dustLimitSatoshis,
@@ -391,11 +390,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
391390
})
392391

393392
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 {
393+
case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelType), open)) =>
394+
Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match {
397395
case Left(t) => handleLocalError(t, d, Some(accept))
398-
case Right(remoteShutdownScript) =>
396+
case Right((channelFeatures, remoteShutdownScript)) =>
399397
val remoteParams = RemoteParams(
400398
nodeId = remoteNodeId,
401399
dustLimit = accept.dustLimitSatoshis,
@@ -1681,7 +1679,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
16811679
val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty
16821680
if (d.commitments.localParams.isFunder && !shutdownInProgress) {
16831681
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
1684-
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, None)
1682+
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, None)
16851683
if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) {
16861684
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
16871685
}
@@ -1972,11 +1970,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
19721970
}
19731971

19741972
private def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = {
1975-
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, Some(c))
1973+
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c))
19761974
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
19771975
val shouldUpdateFee = d.commitments.localParams.isFunder && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)
19781976
val shouldClose = !d.commitments.localParams.isFunder &&
1979-
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelFeatures, networkFeeratePerKw, currentFeeratePerKw) &&
1977+
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) &&
19801978
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
19811979
if (shouldUpdateFee) {
19821980
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
@@ -1996,11 +1994,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
19961994
* @return
19971995
*/
19981996
private def handleOfflineFeerate(c: CurrentFeerates, d: HasCommitments) = {
1999-
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, Some(c))
1997+
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c))
20001998
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
20011999
// if the network fees are too high we risk to not be able to confirm our current commitment
20022000
val shouldClose = networkFeeratePerKw > currentFeeratePerKw &&
2003-
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelFeatures, networkFeeratePerKw, currentFeeratePerKw) &&
2001+
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) &&
20042002
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
20052003
if (shouldClose) {
20062004
if (nodeParams.onChainFeeConf.closeOnOfflineMismatch) {

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

Lines changed: 2 additions & 2 deletions
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: ChannelType)
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: ChannelType)
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

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, ourChannelType: Features, theirChannelType: Features) extends ChannelException(channelId, s"invalid channel_type=0x${theirChannelType.toByteVector.toHex}, expected 0x${ourChannelType.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")

0 commit comments

Comments
 (0)