Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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