Skip to content

Commit

Permalink
Add channel type feature bit (#2073)
Browse files Browse the repository at this point in the history
We already support channel types, but we make it explicit with a feature
bit as required by lightning/bolts#906
  • Loading branch information
t-bast committed Dec 2, 2021
1 parent 86aed63 commit bacb31c
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 26 deletions.
1 change: 1 addition & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ eclair {
option_anchors_zero_fee_htlc_tx = disabled
option_shutdown_anysegwit = optional
option_onion_messages = disabled
option_channel_type = optional
trampoline_payment = disabled
keysend = disabled
}
Expand Down
8 changes: 7 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ object Features {
val mandatory = 38
}

case object ChannelType extends Feature {
val rfcName = "option_channel_type"
val mandatory = 44
}

// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
// We're not advertising these bits yet in our announcements, clients have to assume support.
// This is why we haven't added them yet to `areSupported`.
Expand All @@ -231,12 +236,13 @@ object Features {
PaymentSecret,
BasicMultiPartPayment,
Wumbo,
TrampolinePayment,
StaticRemoteKey,
AnchorOutputs,
AnchorOutputsZeroFeeHtlcTx,
ShutdownAnySegwit,
OnionMessages,
ChannelType,
TrampolinePayment,
KeySend
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ object NodeParams extends Logging {
require(features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory)), s"${Features.VariableLengthOnion.rfcName} must be enabled and mandatory")
require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), s"${Features.PaymentSecret.rfcName} must be enabled and mandatory")
require(!features.hasFeature(Features.InitialRoutingSync), s"${Features.InitialRoutingSync.rfcName} is not supported anymore, use ${Features.ChannelRangeQueries.rfcName} instead")
require(features.hasFeature(Features.ChannelType), s"${Features.ChannelType.rfcName} must be enabled")
}

val pluginMessageParams = pluginParams.collect { case p: CustomFeaturePlugin => p }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ case class InvalidFundingAmount (override val channelId: Byte
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, ourChannelType: ChannelType, theirChannelType: ChannelType) extends ChannelException(channelId, s"invalid channel_type=$theirChannelType, expected channel_type=$ourChannelType")
case class MissingChannelType (override val channelId: ByteVector32) extends ChannelException(channelId, "option_channel_type was negotiated but channel_type is missing")
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 @@ -156,13 +156,16 @@ object Helpers {
*/
def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features, remoteFeatures: Features, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = {
accept.channelType_opt match {
case Some(theirChannelType) if accept.channelType_opt != open.channelType_opt =>
// if channel_type is set, and channel_type was set in open_channel, and they are not equal types: MUST reject the channel.
return Left(InvalidChannelType(open.temporaryChannelId, channelType, theirChannelType))
case None if Features.canUseFeature(localFeatures, remoteFeatures, Features.ChannelType) =>
// Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type`
return Left(MissingChannelType(open.temporaryChannelId))
case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures) =>
// 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.
return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures)))
case Some(theirChannelType) if accept.channelType_opt != open.channelType_opt =>
// if channel_type is set, and channel_type was set in open_channel, and they are not equal types: MUST reject the channel.
return Left(InvalidChannelType(open.temporaryChannelId, channelType, theirChannelType))
case _ => // we agree on channel type
}

Expand Down
10 changes: 6 additions & 4 deletions eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,16 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA
d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match {
case None =>
val channelConfig = ChannelConfig.standard
val chosenChannelType: Either[InvalidChannelType, SupportedChannelType] = msg.channelType_opt match {
// remote doesn't specify a channel type: we use spec-defined defaults
case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures))
// remote explicitly specifies a channel type: we negotiate
val chosenChannelType: Either[ChannelException, SupportedChannelType] = msg.channelType_opt match {
// remote explicitly specifies a channel type: we check whether we want to allow it
case Some(remoteChannelType) => ChannelTypes.areCompatible(d.localFeatures, remoteChannelType) match {
case Some(acceptedChannelType) => Right(acceptedChannelType)
case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures), remoteChannelType))
}
// Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type`
case None if Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.ChannelType) => Left(MissingChannelType(msg.temporaryChannelId))
// remote doesn't specify a channel type: we use spec-defined defaults
case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures))
}
chosenChannelType match {
case Right(channelType) =>
Expand Down
33 changes: 27 additions & 6 deletions eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package fr.acinq.eclair
import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Block, SatoshiLong}
import fr.acinq.eclair.FeatureSupport.Mandatory
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.blockchain.fee.{DustTolerance, FeeratePerByte, FeeratePerKw, FeerateTolerance}
import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager}
Expand Down Expand Up @@ -94,51 +94,71 @@ class StartupSpec extends AnyFunSuite {
s"features.${OptionDataLossProtect.rfcName}" -> "optional",
s"features.${ChannelRangeQueries.rfcName}" -> "optional",
s"features.${ChannelRangeQueriesExtended.rfcName}" -> "optional",
s"features.${ChannelType.rfcName}" -> "optional",
s"features.${VariableLengthOnion.rfcName}" -> "mandatory",
s"features.${PaymentSecret.rfcName}" -> "mandatory",
s"features.${BasicMultiPartPayment.rfcName}" -> "optional"
s"features.${BasicMultiPartPayment.rfcName}" -> "optional",
).asJava)

// var_onion_optin cannot be disabled
val noVariableLengthOnionConf = ConfigFactory.parseMap(Map(
s"features.${OptionDataLossProtect.rfcName}" -> "optional",
s"features.${ChannelRangeQueries.rfcName}" -> "optional",
s"features.${ChannelRangeQueriesExtended.rfcName}" -> "optional"
s"features.${ChannelRangeQueriesExtended.rfcName}" -> "optional",
s"features.${ChannelType.rfcName}" -> "optional",
).asJava)

// var_onion_optin cannot be optional
val optionalVarOnionOptinConf = ConfigFactory.parseMap(Map(
s"features.${OptionDataLossProtect.rfcName}" -> "optional",
s"features.${VariableLengthOnion.rfcName}" -> "optional"
s"features.${ChannelType.rfcName}" -> "optional",
s"features.${VariableLengthOnion.rfcName}" -> "optional",
s"features.${PaymentSecret.rfcName}" -> "mandatory",
).asJava)

// payment_secret cannot be optional
val optionalPaymentSecretConf = ConfigFactory.parseMap(Map(
s"features.${OptionDataLossProtect.rfcName}" -> "optional",
s"features.${ChannelType.rfcName}" -> "optional",
s"features.${VariableLengthOnion.rfcName}" -> "mandatory",
s"features.${PaymentSecret.rfcName}" -> "optional",
).asJava)

// option_channel_type cannot be disabled
val noChannelTypeConf = ConfigFactory.parseMap(Map(
s"features.${OptionDataLossProtect.rfcName}" -> "optional",
s"features.${ChannelRangeQueries.rfcName}" -> "optional",
s"features.${ChannelRangeQueriesExtended.rfcName}" -> "optional",
s"features.${VariableLengthOnion.rfcName}" -> "mandatory",
s"features.${PaymentSecret.rfcName}" -> "mandatory",
s"features.${BasicMultiPartPayment.rfcName}" -> "optional",
).asJava)

// initial_routing_sync cannot be enabled
val initialRoutingSyncConf = ConfigFactory.parseMap(Map(
s"features.${OptionDataLossProtect.rfcName}" -> "optional",
s"features.${InitialRoutingSync.rfcName}" -> "optional",
s"features.${ChannelRangeQueries.rfcName}" -> "optional",
s"features.${ChannelRangeQueriesExtended.rfcName}" -> "optional",
s"features.${ChannelType.rfcName}" -> "optional",
s"features.${VariableLengthOnion.rfcName}" -> "mandatory",
s"features.${PaymentSecret.rfcName}" -> "mandatory",
).asJava)

// extended channel queries without channel queries
val illegalFeaturesConf = ConfigFactory.parseMap(Map(
s"features.${OptionDataLossProtect.rfcName}" -> "optional",
s"features.${ChannelRangeQueriesExtended.rfcName}" -> "optional"
s"features.${ChannelRangeQueriesExtended.rfcName}" -> "optional",
s"features.${ChannelType.rfcName}" -> "optional",
s"features.${VariableLengthOnion.rfcName}" -> "mandatory",
s"features.${PaymentSecret.rfcName}" -> "mandatory",
).asJava)

assert(Try(makeNodeParamsWithDefaults(finalizeConf(legalFeaturesConf))).isSuccess)
assert(Try(makeNodeParamsWithDefaults(finalizeConf(noVariableLengthOnionConf))).isFailure)
assert(Try(makeNodeParamsWithDefaults(finalizeConf(optionalVarOnionOptinConf))).isFailure)
assert(Try(makeNodeParamsWithDefaults(finalizeConf(optionalPaymentSecretConf))).isFailure)
assert(Try(makeNodeParamsWithDefaults(finalizeConf(noChannelTypeConf))).isFailure)
assert(Try(makeNodeParamsWithDefaults(finalizeConf(initialRoutingSyncConf))).isFailure)
assert(Try(makeNodeParamsWithDefaults(finalizeConf(illegalFeaturesConf))).isFailure)
}
Expand All @@ -153,6 +173,7 @@ class StartupSpec extends AnyFunSuite {
| var_onion_optin = mandatory
| payment_secret = mandatory
| basic_mpp = mandatory
| option_channel_type = optional
| }
| }
| ]
Expand All @@ -161,7 +182,7 @@ class StartupSpec extends AnyFunSuite {

val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))
val perNodeFeatures = nodeParams.featuresFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory))
assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional))
}

test("override feerate mismatch tolerance") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ object ChannelStateTestsTags {
val HighDustLimitDifferenceAliceBob = "high_dust_limit_difference_alice_bob"
/** If set, Bob will have a much higher dust limit than Alice. */
val HighDustLimitDifferenceBobAlice = "high_dust_limit_difference_bob_alice"
/** If set, channels will use option_channel_type. */
val ChannelType = "option_channel_type"
}

trait ChannelStateTestsHelperMethods extends TestKitBase {
Expand Down Expand Up @@ -142,13 +144,15 @@ trait ChannelStateTestsHelperMethods extends TestKitBase {
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.OptionUpfrontShutdownScript, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional))
val bobInitFeatures = Bob.nodeParams.features
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.OptionUpfrontShutdownScript, FeatureSupport.Optional))
.modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional))

val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS
aliceOrigin.expectNoMessage()
}

test("recv AcceptChannel (channel type not set but feature bit set)", Tag(ChannelStateTestsTags.ChannelType), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptChannel]
assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))
bob2alice.forward(alice, accept.copy(tlvStream = TlvStream.empty))
alice2bob.expectMsg(Error(accept.temporaryChannelId, "option_channel_type was negotiated but channel_type is missing"))
awaitCond(alice.stateName == CLOSED)
aliceOrigin.expectMsgType[Status.Failure]
}

test("recv AcceptChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f =>
import f._
val accept = bob2alice.expectMsgType[AcceptChannel]
Expand Down
Loading

0 comments on commit bacb31c

Please sign in to comment.