From 0b1dae47a3272dd293cbcfe89a064079e1aa1c5f Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 23 Nov 2021 11:58:03 +0100 Subject: [PATCH 1/3] Add channel type feature bit We already support channel types, but we make it explicit with a feature bit as required by https://github.com/lightning/bolts/pull/906 --- eclair-core/src/main/resources/reference.conf | 1 + eclair-core/src/main/scala/fr/acinq/eclair/Features.scala | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index fef81d4392..d899adc339 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -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 } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 4c392d5642..03b2316277 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -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`. @@ -231,12 +236,13 @@ object Features { PaymentSecret, BasicMultiPartPayment, Wumbo, - TrampolinePayment, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx, ShutdownAnySegwit, OnionMessages, + ChannelType, + TrampolinePayment, KeySend ) From 65f96b851806c8c522d036d634537380704befe8 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 2 Dec 2021 16:52:48 +0100 Subject: [PATCH 2/3] Ensure channel type feature is always activated --- .../scala/fr/acinq/eclair/NodeParams.scala | 1 + .../scala/fr/acinq/eclair/StartupSpec.scala | 33 +++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 0de805f9c4..fd5fb25611 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -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 } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index d4a7709b9b..9e788109a4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -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} @@ -94,37 +94,53 @@ 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) @@ -132,13 +148,17 @@ class StartupSpec extends AnyFunSuite { // 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) } @@ -153,6 +173,7 @@ class StartupSpec extends AnyFunSuite { | var_onion_optin = mandatory | payment_secret = mandatory | basic_mpp = mandatory + | option_channel_type = optional | } | } | ] @@ -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") { From 74baf90d44669b7467d21f567e5f58bd300c7014 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 2 Dec 2021 17:28:23 +0100 Subject: [PATCH 3/3] Verify that remote sets channel type When the feature bit is set, they must provide a channel type. --- .../eclair/channel/ChannelExceptions.scala | 1 + .../fr/acinq/eclair/channel/Helpers.scala | 9 +++-- .../main/scala/fr/acinq/eclair/io/Peer.scala | 10 +++--- .../ChannelStateTestsHelperMethods.scala | 4 +++ .../a/WaitForAcceptChannelStateSpec.scala | 10 ++++++ .../scala/fr/acinq/eclair/io/PeerSpec.scala | 36 ++++++++++++------- 6 files changed, 51 insertions(+), 19 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index c0178227d1..c935a770d5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -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") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index aea99c92f8..486acf0e9c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -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 } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index ca7235088b..8a50e1f83b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -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) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 075246e793..90518bbf1a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -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 { @@ -142,6 +144,7 @@ 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)) @@ -149,6 +152,7 @@ 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 channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index a302e51847..cc5138ac00 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -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] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 2dcd02612d..0d110ffad3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -23,13 +23,14 @@ import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, Btc, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} -import fr.acinq.eclair.Features.{AnchorOutputs, AnchorOutputsZeroFeeHtlcTx, StaticRemoteKey, Wumbo} +import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.ChannelTypes.UnsupportedChannelType import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.states.ChannelStateTestsTags import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol @@ -66,6 +67,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle import com.softwaremill.quicklens._ val aliceParams = TestConstants.Alice.nodeParams + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.ChannelType))(Features(ChannelType -> Optional)) .modify(_.features).setToIf(test.tags.contains("static_remotekey"))(Features(StaticRemoteKey -> Optional)) .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Wumbo -> Optional)) .modify(_.features).setToIf(test.tags.contains("anchor_outputs"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional)) @@ -250,7 +252,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard) assert(peer.stateData.channels.isEmpty) - val open = protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0) + val open = createOpenChannelMessage() peerConnection.send(peer, open) awaitCond(peer.stateData.channels.nonEmpty) assert(channel.expectMsgType[INPUT_INIT_FUNDEE].temporaryChannelId === open.temporaryChannelId) @@ -313,42 +315,48 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle // They only support anchor outputs and we don't. { - val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs)) - val open = protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0, openTlv) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard")) } // They only support anchor outputs with zero fee htlc txs and we don't. { - val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) - val open = protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0, openTlv) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs_zero_fee_htlc_tx, expected channel_type=standard")) } // They want to use a channel type that doesn't exist in the spec. { - val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(AnchorOutputs -> Optional)))) - val open = protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0, openTlv) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(AnchorOutputs -> Optional))))) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=0x200000, expected channel_type=standard")) } // They want to use a channel type we don't support yet. { - val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(Map[Feature, FeatureSupport](StaticRemoteKey -> Mandatory), Set(UnknownFeature(22)))))) - val open = protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0, openTlv) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(Map[Feature, FeatureSupport](StaticRemoteKey -> Mandatory), Set(UnknownFeature(22))))))) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=0x401000, expected channel_type=standard")) } } + test("don't spawn a channel is channel type is missing with the feature bit set", Tag(ChannelStateTestsTags.ChannelType)) { f => + import f._ + + val remoteInit = protocol.Init(Features(ChannelType -> Optional)) + connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = remoteInit) + assert(peer.stateData.channels.isEmpty) + val open = createOpenChannelMessage() + peerConnection.send(peer, open) + peerConnection.expectMsg(Error(open.temporaryChannelId, "option_channel_type was negotiated but channel_type is missing")) + } + test("use their channel type when spawning a channel", Tag("static_remotekey")) { f => import f._ // We both support option_static_remotekey but they want to open a standard channel. connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional))) assert(peer.stateData.channels.isEmpty) - val openTlv = TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard)) - val open = protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0, openTlv) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard))) peerConnection.send(peer, open) awaitCond(peer.stateData.channels.nonEmpty) assert(channel.expectMsgType[INPUT_INIT_FUNDEE].channelType === ChannelTypes.Standard) @@ -468,4 +476,8 @@ object PeerSpec { (mockServer, mockServer.getLocalAddress.asInstanceOf[InetSocketAddress]) } + def createOpenChannelMessage(openTlv: TlvStream[OpenChannelTlv] = TlvStream.empty): protocol.OpenChannel = { + protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0, openTlv) + } + } \ No newline at end of file